1
0
mirror of https://github.com/jbranchaud/til synced 2026-07-05 17:00:17 +00:00

Compare commits

..

88 Commits

Author SHA1 Message Date
jbranchaud facc606014 Add Get Quotient And Remainder In One Operation as a Python TIL 2026-05-05 16:57:07 -05:00
jbranchaud be103b52dd Add Programmatically Grab SHA For Head Commit as a Git TIL 2026-05-04 16:12:31 -05:00
jbranchaud 3402428aad Add Reclassify Certain Packagaes As Dev Dependencies as a Python TIL 2026-05-04 14:30:12 -05:00
jbranchaud 0c00e47141 Add Open File To Specific Line In Browser as a GitHub TIL 2026-05-03 19:18:14 -05:00
jbranchaud 1c90fdd823 Add Get Absolute Seconds From timedelta Object as a Python TIL 2026-05-02 16:56:49 -05:00
jbranchaud bf3991ce04 Add a missing link to the latest TIL 2026-05-02 12:37:50 -05:00
jbranchaud 3db5af78c1 Add View Nicely Formatted Markdown From Terminal as a Workflow TIL 2026-05-02 12:36:15 -05:00
jbranchaud a8c35e2458 Add Define Sequence Of Tests With Parametrize Decorator as a Python TIL 2026-05-01 21:30:20 -05:00
jbranchaud c0ad3cee4d Add Assert Is Only A Development Check as a Python TIL 2026-05-01 17:01:14 -05:00
jbranchaud 9bd1bb413a Add Reverse Each Line Of A File as a Unix TIL 2026-05-01 16:07:15 -05:00
jbranchaud ab8331000f Add Sort Normalized Version Of Data as a Python TIL 2026-04-29 11:34:32 -05:00
jbranchaud cd54360925 Add missing terminal prompt in code block 2026-04-23 12:24:23 -05:00
jbranchaud 75421685ea Add List PRs Awaiting Your Review as a GitHub TIL 2026-04-22 19:53:32 -05:00
jbranchaud 0cb5890fc0 Add Access Variables Outside Loop Scope as a Python TIL 2026-04-21 17:48:06 -05:00
jbranchaud 7de0e70d78 Add Define A Set Of Class Methods as a Ruby TIL 2026-04-17 10:51:31 -04:00
jbranchaud 36934aa56f Add Make Dataclass Sortable By Specific Field as a Python TIL 2026-04-15 22:53:29 -05:00
jbranchaud 2cd465bb08 Add Display All Git Log Entries In My Local Timezone as a Git TIL 2026-04-09 12:54:59 -05:00
jbranchaud 6ad376885b Add Stash The Current Prompt To Send Another First as a Claude Code TIL 2026-04-08 13:35:11 -05:00
jbranchaud 0c4702be97 Add Count Number Of Tokens In A File as an LLM TIL 2026-04-03 09:23:11 -05:00
jbranchaud b873f86f5b Add Sort A List Of Dataclass Instances as a Python TIL 2026-04-01 20:38:20 -05:00
jbranchaud 1120bb2018 Add Avoid Vulnerabilities In New Package Versions as a PNPM TIL 2026-03-31 11:33:13 -05:00
jbranchaud 906253b7dc Add List Available Zle Keybindings as a Zsh TIL 2026-03-30 13:45:35 -05:00
jbranchaud b4920c0397 Add Skip Specific Pytest Test Cases as a Python TIL 2026-03-29 11:05:32 -05:00
jbranchaud 119cc15c9a Add a couple more examples to most recent TIL 2026-03-29 01:22:39 -05:00
jbranchaud 1a4589f8f7 Add Use The Readline Keybindings Anywhere as a Unix TIL 2026-03-27 22:38:14 -05:00
jbranchaud 5f35404433 Add Avoid Modification With Frozen Dataclass as a Python TIL 2026-03-25 18:52:19 -05:00
jbranchaud 1766e45134 Add Start The Debugger When A Test Errors as a Python TIL 2026-03-23 21:27:10 -05:00
jbranchaud c875652725 Add Add Default Task To List All Tasks as a Taskfile TIL 2026-03-23 21:12:44 -05:00
jbranchaud 8af252f232 Add Use __post_init__ For dataclass Validations as a Python TIL 2026-03-22 14:37:59 -05:00
jbranchaud eb0a7e1b3d Link to the VisualMode blog from the README 2026-03-21 13:28:25 -05:00
jbranchaud c1cd40311f Add Browse And Search Help Docs as a Unix TIL 2026-03-21 12:34:49 -05:00
jbranchaud c744117eff Add Deduplicate A List Into A Tuple as a Python TIL 2026-03-21 12:33:09 -05:00
jbranchaud 329ce1aa3e Add Filter By Type as a Ruby TIL 2026-03-20 09:18:10 -05:00
jbranchaud 16082177aa Add Create A Range Of Descending Values as a Python TIL 2026-03-19 00:25:32 -05:00
jbranchaud 2276a57445 Add Look Inside Pytest tmp_path as a Python TIL 2026-03-18 21:03:51 -05:00
jbranchaud ceaab3da4f Add Reveal Location Of File In Finder.app as a Mac TIL 2026-03-16 09:40:18 -05:00
jbranchaud c7711ca337 Add Control Passing Of Time In Tests as a Python TIL 2026-03-14 16:18:36 -05:00
jbranchaud 11279ac362 Add Load A Module And Execute A Statement as a Ruby TIL 2026-03-14 15:55:09 -05:00
jbranchaud 01f9d89e8e Add Parse Relative Time To datetime Object as a Python TIL 2026-03-08 10:53:04 -05:00
jbranchaud e1c3f23975 Add Pick From Tasks Using Interactive Picker as a Mise TIL 2026-03-07 23:40:52 -06:00
jbranchaud 16ad6bd64d Fix typo in old mise TIL 2026-03-07 17:03:44 -06:00
jbranchaud f2a6fddba8 Add Use Verbose Flag To Get More Diff as a Python TIL 2026-03-06 09:32:05 -06:00
jbranchaud e2603f1445 Add Undo Latest Changes Committed To Specific File as a Git TIL 2026-03-05 14:53:55 -06:00
jbranchaud c1f046d196 Add Iterate Over A Dictionary as a Python TIL 2026-03-04 00:20:58 -06:00
jbranchaud 55a6691681 Add Combine All My TILs Into A Single File as a Unix TIL 2026-03-01 12:06:07 -06:00
jbranchaud 3632cfbe1b Add Easy Key-Value Aggregates With defaultdict as a Python TIL 2026-02-26 21:51:08 -06:00
jbranchaud e82bc873b9 Add Set, Get, And Unset Env Vars With Dokku as a Devops TIL 2026-02-25 19:05:40 -06:00
jbranchaud 43ade88fab Add Access Most Recent Return Value In REPL as a Python TIL 2026-02-24 20:37:12 -06:00
jbranchaud 8f99085e4b Add Edit The Current Command Prompt as a Bash TIL 2026-02-23 20:52:05 -06:00
jbranchaud f20428b06a Add Keep A Tally With collections.Counter as a Python TIL 2026-02-22 14:28:37 -06:00
jbranchaud e41802653d Add Inspect EXIF Data For An Image File as a Unix TIL 2026-02-22 11:52:47 -06:00
jbranchaud 2cc52bf8bc Add Search Through Bin Paths For Tool Locations as a mise TIL 2026-02-22 10:53:56 -06:00
jbranchaud df418b5718 Add Load A File Into The Python REPL as a Python TIL 2026-02-21 16:58:57 -06:00
jbranchaud d084e0ffe0 Add Check If Package Is Installed With Pip as a Python TIL 2026-02-19 13:54:50 -06:00
jbranchaud 72b466a8b3 Add Override Your Project Mise File as a Mise TIL 2026-02-18 15:33:46 -06:00
jbranchaud be18f387ed Add Install With PIP For Specific Interpreter as a Python TIL 2026-02-16 14:40:27 -06:00
jbranchaud efb83050ab Add Iterate First N Items From Enumerable as a Python TIL 2026-02-16 13:32:35 -06:00
jbranchaud ec12f7ea80 Add Make A Long String Of Text Readable as a Ruby TIL 2026-02-12 17:30:15 -06:00
jbranchaud f186d5977d Add Compute Median Instead of Average as a PostgreSQL TIL 2026-02-02 16:55:50 -06:00
jbranchaud f967520fa3 Add Specify Default For Data Definition as a Ruby TIL 2026-02-02 08:26:05 -06:00
jbranchaud 1517e1fb7a Add Check How Database Is Configured as a Rails TIL 2026-01-30 13:26:12 -06:00
jbranchaud f56d93b49b Add Control Which Monitor App Switcher Appears On as a Mac TIL 2026-01-27 13:46:08 -06:00
jbranchaud dd6350aa41 Add Check The Current Named Log Level as a Rails TIL 2026-01-19 13:52:27 -06:00
jbranchaud bdd3adf577 Add Format And Display Small Amounts Of Columnar Data as a Unix TIL 2026-01-19 13:42:42 -06:00
jbranchaud f48adc0f05 Add Clean Up Memory-Hungry Rails Console Processes as a Rails TIL 2026-01-18 15:09:22 -06:00
jbranchaud 9773f10b84 Add Use Negative Lookbehind Matching With ripgrep as a Unix TIL 2026-01-15 08:32:41 -06:00
jbranchaud bd58be8fda Remove dot character from copy-pasting terminal output 2026-01-13 14:01:40 -06:00
jbranchaud 35f1f0b807 Add List Processes Running Across All Session as a tmux TIL 2026-01-13 14:00:21 -06:00
jbranchaud 81afd44913 Add Show Tree View Of Processes And Subprocesses as a Unix TIL 2026-01-13 11:11:43 -06:00
jbranchaud 6fdadfa1fb Add Resume Specific Session as a Claude Code TIL 2026-01-12 21:37:13 -06:00
jbranchaud aaf7da413c Add Hide Overflowing Text For Google Sheets Column as an Internet TIL 2026-01-10 14:04:11 -06:00
jbranchaud 6a2a0a8ac1 Add Diff Two Files In Unified Format as a Unix TIL 2026-01-10 13:28:46 -06:00
jbranchaud 712fc66aae Add Skip Git Hooks As Needed as a Git TIL 2026-01-09 21:23:55 -06:00
jbranchaud e95477607e Remove notes:pull task since it's redundant 2026-01-09 21:01:50 -06:00
jbranchaud 087766a792 Add Apply Successive Filters To Lines In Less as a Unix TIL 2026-01-07 19:14:52 -06:00
jbranchaud 4801e730f9 Add Allow Edits From The Start as a Claude Code TIL 2026-01-07 10:51:45 -06:00
jbranchaud bd021f7eab Add Check Ruby Version For Production App as a Heroku TIL 2026-01-04 20:30:59 -06:00
jbranchaud 8d8cfd56ce Add Determine Absolute Path Of Top-Level Project Directory as a Git TIL 2026-01-03 16:36:37 -06:00
jbranchaud f4faa06258 Add another useful link to recent TIL 2026-01-03 12:53:33 -06:00
jbranchaud 8ccbd82320 Add Display Line Numbers While Using Less as a Unix TIL 2026-01-02 18:40:28 -07:00
jbranchaud 5ea4165893 Add Look In Ruby Version Dotfile as a Mise TIL 2026-01-01 16:00:46 -07:00
jbranchaud 73476a8d16 Add Install From Nonstandard Brewfile as a Brew TIL 2026-01-01 12:16:51 -07:00
jbranchaud c5ce81f918 Update README to reflect current work 2026-01-01 12:06:16 -07:00
jbranchaud 32be787998 Update copyright year to 2026 2026-01-01 11:51:51 -07:00
jbranchaud 1d835d3553 Simplify notes:sync to pull directly into local main
Was fetching remote then checking out stale local branch
2026-01-01 11:51:23 -07:00
copilot-swe-agent[bot] 0d4959046d Reorder commands: commit before pull --rebase
Co-authored-by: jbranchaud <694063+jbranchaud@users.noreply.github.com>
2025-12-31 17:28:33 -07:00
copilot-swe-agent[bot] b1198d2488 Simplify pull command to use configured upstream
Co-authored-by: jbranchaud <694063+jbranchaud@users.noreply.github.com>
2025-12-31 17:28:33 -07:00
copilot-swe-agent[bot] 6c3805e7cd Add git pull --rebase to notes:push task
Co-authored-by: jbranchaud <694063+jbranchaud@users.noreply.github.com>
2025-12-31 17:28:33 -07:00
77 changed files with 3071 additions and 15 deletions
+82 -3
View File
@@ -6,14 +6,15 @@ A collection of concise write-ups on small things I learn day to day across a
variety of languages and technologies. These are things that don't really variety of languages and technologies. These are things that don't really
warrant a full blog post. These are things I've picked up by [Learning In warrant a full blog post. These are things I've picked up by [Learning In
Public™](https://dev.to/jbranchaud/how-i-built-a-learning-machine-45k9) and Public™](https://dev.to/jbranchaud/how-i-built-a-learning-machine-45k9) and
pairing with smart people at Hashrocket. working across different projects via [VisualMode](https://www.visualmode.dev/).
For a steady stream of TILs, [sign up for my newsletter](https://visualmode.kit.com/newsletter). For a steady stream of TILs, [sign up for my newsletter](https://visualmode.kit.com/newsletter).
_1715 TILs and counting..._ _1789 TILs and counting..._
See some of the other learning resources I work on: See some of the other learning resources I work on:
- [The VisualMode Blog](https://visualmode.dev/blog)
- [Get Started with Vimium](https://egghead.io/courses/get-started-with-vimium~3t5f7) - [Get Started with Vimium](https://egghead.io/courses/get-started-with-vimium~3t5f7)
- [Ruby Operator Lookup](https://www.visualmode.dev/ruby-operators) - [Ruby Operator Lookup](https://www.visualmode.dev/ruby-operators)
- [Vim Un-Alphabet](https://www.youtube.com/playlist?list=PL46-cKSxMYYCMpzXo6p0Cof8hJInYgohU) - [Vim Un-Alphabet](https://www.youtube.com/playlist?list=PL46-cKSxMYYCMpzXo6p0Cof8hJInYgohU)
@@ -29,6 +30,7 @@ If you've learned something here, support my efforts writing daily TILs by
* [Ansible](#ansible) * [Ansible](#ansible)
* [Astro](#astro) * [Astro](#astro)
* [AWS](#aws) * [AWS](#aws)
* [Bash](#bash)
* [Brew](#brew) * [Brew](#brew)
* [Chrome](#chrome) * [Chrome](#chrome)
* [Claude Code](#claude-code) * [Claude Code](#claude-code)
@@ -126,11 +128,16 @@ If you've learned something here, support my efforts writing daily TILs by
- [Turn Off Output Pager For A Command](aws/turn-off-output-pager-for-a-command.md) - [Turn Off Output Pager For A Command](aws/turn-off-output-pager-for-a-command.md)
- [Use Specific AWS Profile With CLI](aws/use-specific-aws-profile-with-cli.md) - [Use Specific AWS Profile With CLI](aws/use-specific-aws-profile-with-cli.md)
### Bash
- [Edit The Current Command Prompt](bash/edit-the-current-command-prompt.md)
### Brew ### Brew
- [Clean Up Your Brew Installations](brew/clean-up-your-brew-installations.md) - [Clean Up Your Brew Installations](brew/clean-up-your-brew-installations.md)
- [Configure Brew Environment Variables](brew/configure-brew-environment-variables.md) - [Configure Brew Environment Variables](brew/configure-brew-environment-variables.md)
- [Export List Of Everything Installed By Brew](brew/export-list-of-everything-installed-by-brew.md) - [Export List Of Everything Installed By Brew](brew/export-list-of-everything-installed-by-brew.md)
- [Install From Nonstandard Brewfile](brew/install-from-nonstandard-brewfile.md)
- [Install Go Packages In Brewfile](brew/install-go-packages-in-brewfile.md) - [Install Go Packages In Brewfile](brew/install-go-packages-in-brewfile.md)
- [List All Services Managed By Brew](brew/list-all-services-managed-by-brew.md) - [List All Services Managed By Brew](brew/list-all-services-managed-by-brew.md)
@@ -157,8 +164,11 @@ If you've learned something here, support my efforts writing daily TILs by
### Claude Code ### Claude Code
- [Allow Edits From The Start](claude-code/allow-edits-from-the-start.md)
- [Monitor Usage Limits From CLI](claude-code/monitor-usage-limits-from-cli.md) - [Monitor Usage Limits From CLI](claude-code/monitor-usage-limits-from-cli.md)
- [Open Current Prompt In Default Editor](claude-code/open-current-prompt-in-default-editor.md) - [Open Current Prompt In Default Editor](claude-code/open-current-prompt-in-default-editor.md)
- [Resume Specific Session](claude-code/resume-specific-session.md)
- [Stash The Current Prompt To Send Another First](claude-code/stash-the-current-prompt-to-send-another-first.md)
### Clojure ### Clojure
@@ -238,6 +248,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [Reload The nginx Configuration](devops/reload-the-nginx-configuration.md) - [Reload The nginx Configuration](devops/reload-the-nginx-configuration.md)
- [Resolve The Public IP Of A URL](devops/resolve-the-public-ip-of-a-url.md) - [Resolve The Public IP Of A URL](devops/resolve-the-public-ip-of-a-url.md)
- [Running Out Of inode Space](devops/running-out-of-inode-space.md) - [Running Out Of inode Space](devops/running-out-of-inode-space.md)
- [Set, Get, And Unset Env Vars With Dokku](devops/set-get-and-unset-env-vars-with-dokku.md)
- [Set Up Domain For Hatchbox Rails App](devops/set-up-domain-for-hatchbox-rails-app.md) - [Set Up Domain For Hatchbox Rails App](devops/set-up-domain-for-hatchbox-rails-app.md)
- [SSH Into A Docker Container](devops/ssh-into-a-docker-container.md) - [SSH Into A Docker Container](devops/ssh-into-a-docker-container.md)
- [SSL Certificates Can Cover Multiple Domains](devops/ssl-certificates-can-cover-multiple-domains.md) - [SSL Certificates Can Cover Multiple Domains](devops/ssl-certificates-can-cover-multiple-domains.md)
@@ -348,8 +359,10 @@ If you've learned something here, support my efforts writing daily TILs by
- [Count Number Of Commits On A Branch](git/count-number-of-commits-on-a-branch.md) - [Count Number Of Commits On A Branch](git/count-number-of-commits-on-a-branch.md)
- [Create A New Branch With Git Switch](git/create-a-new-branch-with-git-switch.md) - [Create A New Branch With Git Switch](git/create-a-new-branch-with-git-switch.md)
- [Delete All Untracked Files](git/delete-all-untracked-files.md) - [Delete All Untracked Files](git/delete-all-untracked-files.md)
- [Determine Absolute Path Of Top-Level Project Directory](git/determine-absolute-path-of-top-level-project-directory.md)
- [Determine The Hash Id For A Blob](git/determine-the-hash-id-for-a-blob.md) - [Determine The Hash Id For A Blob](git/determine-the-hash-id-for-a-blob.md)
- [Diffing With Patience](git/diffing-with-patience.md) - [Diffing With Patience](git/diffing-with-patience.md)
- [Display All Git Log Entries In My Local Timezone](git/display-all-git-log-entries-in-my-local-timezone.md)
- [Dropping Commits With Git Rebase](git/dropping-commits-with-git-rebase.md) - [Dropping Commits With Git Rebase](git/dropping-commits-with-git-rebase.md)
- [Dry Runs in Git](git/dry-runs-in-git.md) - [Dry Runs in Git](git/dry-runs-in-git.md)
- [Exclude A File From A Diff Output](git/exclude-a-file-from-a-diff-output.md) - [Exclude A File From A Diff Output](git/exclude-a-file-from-a-diff-output.md)
@@ -393,6 +406,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [Move The Latest Commit To A New Branch](git/move-the-latest-commit-to-a-new-branch.md) - [Move The Latest Commit To A New Branch](git/move-the-latest-commit-to-a-new-branch.md)
- [Override The Global Git Ignore File](git/override-the-global-git-ignore-file.md) - [Override The Global Git Ignore File](git/override-the-global-git-ignore-file.md)
- [Pick Specific Changes To Stash](git/pick-specific-changes-to-stash.md) - [Pick Specific Changes To Stash](git/pick-specific-changes-to-stash.md)
- [Programmatically Grab SHA For Head Commit](git/programmatically-grab-sha-for-head-commit.md)
- [Pulling In Changes During An Interactive Rebase](git/pulling-in-changes-during-an-interactive-rebase.md) - [Pulling In Changes During An Interactive Rebase](git/pulling-in-changes-during-an-interactive-rebase.md)
- [Push To A Branch On Another Remote](git/push-to-a-branch-on-another-remote.md) - [Push To A Branch On Another Remote](git/push-to-a-branch-on-another-remote.md)
- [Quicker Commit Fixes With The Fixup Flag](git/quicker-commit-fixes-with-the-fixup-flag.md) - [Quicker Commit Fixes With The Fixup Flag](git/quicker-commit-fixes-with-the-fixup-flag.md)
@@ -423,6 +437,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [Show What Is In A Stash](git/show-what-is-in-a-stash.md) - [Show What Is In A Stash](git/show-what-is-in-a-stash.md)
- [Single Key Presses in Interactive Mode](git/single-key-presses-in-interactive-mode.md) - [Single Key Presses in Interactive Mode](git/single-key-presses-in-interactive-mode.md)
- [Skip A Bad Commit When Bisecting](git/skip-a-bad-commit-when-bisecting.md) - [Skip A Bad Commit When Bisecting](git/skip-a-bad-commit-when-bisecting.md)
- [Skip Git Hooks As Needed](git/skip-git-hooks-as-needed.md)
- [Skip Pre-Commit Hooks](git/skip-pre-commit-hooks.md) - [Skip Pre-Commit Hooks](git/skip-pre-commit-hooks.md)
- [Staging Changes Within Vim](git/staging-changes-within-vim.md) - [Staging Changes Within Vim](git/staging-changes-within-vim.md)
- [Staging Stashes Interactively](git/staging-stashes-interactively.md) - [Staging Stashes Interactively](git/staging-stashes-interactively.md)
@@ -434,6 +449,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [Transition A Branch From One Base To Another](git/transition-a-branch-from-one-base-to-another.md) - [Transition A Branch From One Base To Another](git/transition-a-branch-from-one-base-to-another.md)
- [Turn Off The Output Pager For One Command](git/turn-off-the-output-pager-for-one-command.md) - [Turn Off The Output Pager For One Command](git/turn-off-the-output-pager-for-one-command.md)
- [Two Kinds Of Dotted Range Notation](git/two-kinds-of-dotted-range-notation.md) - [Two Kinds Of Dotted Range Notation](git/two-kinds-of-dotted-range-notation.md)
- [Undo Latest Changes Committed To Specific File](git/undo-latest-changes-committed-to-specific-file.md)
- [Unstage Changes Wih Git Restore](git/unstage-changes-with-git-restore.md) - [Unstage Changes Wih Git Restore](git/unstage-changes-with-git-restore.md)
- [Untrack A Directory Of Files Without Deleting](git/untrack-a-directory-of-files-without-deleting.md) - [Untrack A Directory Of Files Without Deleting](git/untrack-a-directory-of-files-without-deleting.md)
- [Untrack A File Without Deleting It](git/untrack-a-file-without-deleting-it.md) - [Untrack A File Without Deleting It](git/untrack-a-file-without-deleting-it.md)
@@ -449,7 +465,9 @@ If you've learned something here, support my efforts writing daily TILs by
### GitHub ### GitHub
- [Access Your GitHub Profile Photo](github/access-your-github-profile-photo.md) - [Access Your GitHub Profile Photo](github/access-your-github-profile-photo.md)
- [List PRs Awaiting Your Review](github/list-prs-awaiting-your-review.md)
- [Open A PR To An Unforked Repo](github/open-a-pr-to-an-unforked-repo.md) - [Open A PR To An Unforked Repo](github/open-a-pr-to-an-unforked-repo.md)
- [Open File To Specific Line In Browser](github/open-file-to-specific-line-in-browser.md)
- [Target Another Repo When Creating A PR](github/target-another-repo-when-creating-a-pr.md) - [Target Another Repo When Creating A PR](github/target-another-repo-when-creating-a-pr.md)
- [Tell gh What The Default Repo Is](github/tell-gh-what-the-default-repo-is.md) - [Tell gh What The Default Repo Is](github/tell-gh-what-the-default-repo-is.md)
@@ -500,6 +518,7 @@ If you've learned something here, support my efforts writing daily TILs by
### Heroku ### Heroku
- [Check Ruby Version For Production App](heroku/check-ruby-version-for-production-app.md)
- [Connect To A Database By Color](heroku/connect-to-a-database-by-color.md) - [Connect To A Database By Color](heroku/connect-to-a-database-by-color.md)
- [Deploy A Review App To A Different Stack](heroku/deploy-a-review-app-to-a-different-stack.md) - [Deploy A Review App To A Different Stack](heroku/deploy-a-review-app-to-a-different-stack.md)
- [Diagnose Problems In A Heroku Postgres Database](heroku/diagnose-problems-in-a-heroku-postgres-database.md) - [Diagnose Problems In A Heroku Postgres Database](heroku/diagnose-problems-in-a-heroku-postgres-database.md)
@@ -545,6 +564,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [Focus The URL Bar](internet/focus-the-url-bar.md) - [Focus The URL Bar](internet/focus-the-url-bar.md)
- [Get Random Images From Unsplash](internet/get-random-images-from-unsplash.md) - [Get Random Images From Unsplash](internet/get-random-images-from-unsplash.md)
- [Grab The RSS Feed For A Substack Blog](internet/grab-the-rss-feed-for-a-substack-blog.md) - [Grab The RSS Feed For A Substack Blog](internet/grab-the-rss-feed-for-a-substack-blog.md)
- [Hide Overflowing Text For Google Sheets Column](internet/hide-overflowing-text-for-google-sheets-column.md)
- [Search Tweets By Author](internet/search-tweets-by-author.md) - [Search Tweets By Author](internet/search-tweets-by-author.md)
- [Show All Pivotal Stories With Blockers](internet/show-all-pivotal-stories-with-blockers.md) - [Show All Pivotal Stories With Blockers](internet/show-all-pivotal-stories-with-blockers.md)
- [Verify Site Ownership With DNS Record](internet/verify-site-ownership-with-dns-record.md) - [Verify Site Ownership With DNS Record](internet/verify-site-ownership-with-dns-record.md)
@@ -701,6 +721,7 @@ If you've learned something here, support my efforts writing daily TILs by
### LLM ### LLM
- [Count Number Of Tokens In A File](llm/count-number-of-tokens-in-a-file.md)
- [Send cURL To Claude Text Completion API](llm/send-curl-to-claude-text-completion-api.md) - [Send cURL To Claude Text Completion API](llm/send-curl-to-claude-text-completion-api.md)
- [Use The llm CLI With Claude Models](llm/use-the-llm-cli-with-claude-models.md) - [Use The llm CLI With Claude Models](llm/use-the-llm-cli-with-claude-models.md)
@@ -713,6 +734,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [Capture Screenshot To Clipboard From CLI](mac/capture-screenshot-to-clipboard-from-cli.md) - [Capture Screenshot To Clipboard From CLI](mac/capture-screenshot-to-clipboard-from-cli.md)
- [Check Network Quality Stats From The Command Line](mac/check-network-quality-stats-from-the-command-line.md) - [Check Network Quality Stats From The Command Line](mac/check-network-quality-stats-from-the-command-line.md)
- [Clean Up Old Homebrew Files](mac/clean-up-old-homebrew-files.md) - [Clean Up Old Homebrew Files](mac/clean-up-old-homebrew-files.md)
- [Control Which Monitor App Switcher Appears On](mac/control-which-monitor-app-switcher-appears-on.md)
- [Convert An HEIC Image File To JPG](mac/convert-an-heic-image-file-to-jpg.md) - [Convert An HEIC Image File To JPG](mac/convert-an-heic-image-file-to-jpg.md)
- [Default Screenshot Location](mac/default-screenshot-location.md) - [Default Screenshot Location](mac/default-screenshot-location.md)
- [Detect How Long A User Has Been Idle](mac/detect-how-long-a-user-has-been-idle.md) - [Detect How Long A User Has Been Idle](mac/detect-how-long-a-user-has-been-idle.md)
@@ -731,6 +753,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [Require Additional JS Libraries In Postman](mac/require-additional-js-libraries-in-postman.md) - [Require Additional JS Libraries In Postman](mac/require-additional-js-libraries-in-postman.md)
- [Resize App Windows With AppleScript](mac/resize-app-windows-with-applescript.md) - [Resize App Windows With AppleScript](mac/resize-app-windows-with-applescript.md)
- [Resizing Both Corners Of A Window](mac/resizing-both-corners-of-a-window.md) - [Resizing Both Corners Of A Window](mac/resizing-both-corners-of-a-window.md)
- [Reveal Location Of File In Finder.app](mac/reveal-location-of-file-in-finder-app.md)
- [Run A Hardware Check](mac/run-a-hardware-check.md) - [Run A Hardware Check](mac/run-a-hardware-check.md)
- [Run AppleScript Commands Inline In The Terminal](mac/run-applescript-commands-inline-in-the-terminal.md) - [Run AppleScript Commands Inline In The Terminal](mac/run-applescript-commands-inline-in-the-terminal.md)
- [Set A Window To Its Default Zoom Level](mac/set-a-window-to-its-default-zoom-level.md) - [Set A Window To Its Default Zoom Level](mac/set-a-window-to-its-default-zoom-level.md)
@@ -746,9 +769,13 @@ If you've learned something here, support my efforts writing daily TILs by
- [Create Umbrella Task For All Test Tasks](mise/create-umbrella-task-for-all-test-tasks.md) - [Create Umbrella Task For All Test Tasks](mise/create-umbrella-task-for-all-test-tasks.md)
- [List The Files Being Loaded By Mise](mise/list-the-files-being-loaded-by-mise.md) - [List The Files Being Loaded By Mise](mise/list-the-files-being-loaded-by-mise.md)
- [Look In Ruby Version Dotfile](mise/look-in-ruby-version-dotfile.md)
- [Override Your Project Mise File](mise/override-your-project-mise-file.md)
- [Pick From Tasks Using Interactive Picker](mise/pick-from-tasks-using-interactive-picker.md)
- [Preserve Color Output For Task Command](mise/preserve-color-output-for-task-command.md) - [Preserve Color Output For Task Command](mise/preserve-color-output-for-task-command.md)
- [Read Existing Dot Env File Into Env Vars](mise/read-existing-dot-env-file-into-env-vars.md) - [Read Existing Dot Env File Into Env Vars](mise/read-existing-dot-env-file-into-env-vars.md)
- [Run A Command With Specific Tool Version](mise/run-a-command-with-specific-tool-version.md) - [Run A Command With Specific Tool Version](mise/run-a-command-with-specific-tool-version.md)
- [Search Through Bin Paths For Tool Locations](mise/search-through-bin-paths-for-tool-locations.md)
### MongoDB ### MongoDB
@@ -826,6 +853,7 @@ If you've learned something here, support my efforts writing daily TILs by
### pnpm ### pnpm
- [Avoid Vulnerabilities In New Package Versions](pnpm/avoid-vulnerabilities-in-new-package-versions.md)
- [Execute A Command From The Workspace Root](pnpm/execute-a-command-from-the-workspace-root.md) - [Execute A Command From The Workspace Root](pnpm/execute-a-command-from-the-workspace-root.md)
- [Install Command Runs For Entire Workspace](pnpm/install-command-runs-for-entire-workspace.md) - [Install Command Runs For Entire Workspace](pnpm/install-command-runs-for-entire-workspace.md)
- [List The Installed Version Of A Specific Package](pnpm/list-the-installed-version-of-a-specific-package.md) - [List The Installed Version Of A Specific Package](pnpm/list-the-installed-version-of-a-specific-package.md)
@@ -854,6 +882,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [Clear The Screen In psql](postgres/clear-the-screen-in-psql.md) - [Clear The Screen In psql](postgres/clear-the-screen-in-psql.md)
- [Clear The Screen In psql (2)](postgres/clear-the-screen-in-psql-2.md) - [Clear The Screen In psql (2)](postgres/clear-the-screen-in-psql-2.md)
- [Compute Hashes With pgcrypto](postgres/compute-hashes-with-pgcrypto.md) - [Compute Hashes With pgcrypto](postgres/compute-hashes-with-pgcrypto.md)
- [Compute Median Instead Of Average](postgres/compute-median-instead-of-average.md)
- [Compute The Levenshtein Distance Of Two Strings](postgres/compute-the-levenshtein-distance-of-two-strings.md) - [Compute The Levenshtein Distance Of Two Strings](postgres/compute-the-levenshtein-distance-of-two-strings.md)
- [Compute The md5 Hash Of A String](postgres/compute-the-md5-hash-of-a-string.md) - [Compute The md5 Hash Of A String](postgres/compute-the-md5-hash-of-a-string.md)
- [Concatenate Strings With A Separator](postgres/concatenate-strings-with-a-separator.md) - [Concatenate Strings With A Separator](postgres/concatenate-strings-with-a-separator.md)
@@ -1021,13 +1050,40 @@ If you've learned something here, support my efforts writing daily TILs by
### Python ### Python
- [Access Instance Variables](python/access-instance-variables.md) - [Access Instance Variables](python/access-instance-variables.md)
- [Access Most Recent Return Value In REPL](python/access-most-recent-return-value-in-repl.md)
- [Access Variables Outside Loop Scope](python/access-variables-outside-loop-scope.md)
- [Assert Is Only A Development Check](python/assert-is-only-a-development-check.md)
- [Avoid Modification With Frozen Dataclass](python/avoid-modification-with-frozen-dataclass.md)
- [Break Debugger On First Line Of Program](python/break-debugger-on-first-line-of-program.md) - [Break Debugger On First Line Of Program](python/break-debugger-on-first-line-of-program.md)
- [Check If Package Is Installed With Pip](python/check-if-package-is-installed-with-pip.md)
- [Control Passing Of Time In Tests](python/control-passing-of-time-in-tests.md)
- [Create A Dummy DataFrame In Pandas](python/create-a-dummy-dataframe-in-pandas.md) - [Create A Dummy DataFrame In Pandas](python/create-a-dummy-dataframe-in-pandas.md)
- [Create A Range Of Descending Values](python/create-a-range-of-descending-values.md)
- [Deduplicate A List Into A Tuple](python/deduplicate-a-list-into-a-tuple.md)
- [Define Sequence Of Tests With Parametrize Decorator](python/define-sequence-of-tests-with-parametrize-decorator.md)
- [Dunder Methods](python/dunder-methods.md) - [Dunder Methods](python/dunder-methods.md)
- [Easy Key-Value Aggregates With defaultdict](python/easy-key-value-aggregates-with-defaultdict.md)
- [Get Absolute Seconds From `timedelta` Object](python/get-absolute-seconds-from-timedelta-object.md)
- [Get Quotient And Remainder In One Operation](python/get-quotient-and-remainder-in-one-operation.md)
- [Install With PIP For Specific Interpreter](python/install-with-pip-for-specific-interpreter.md)
- [Iterate First N Items From Enumerable](python/iterate-first-n-items-from-enumerable.md)
- [Iterate Over A Dictionary](python/iterate-over-a-dictionary.md)
- [Keep A Tally With collections.Counter](python/keep-a-tally-with-collections-counter.md)
- [Load A File Into The Python REPL](python/load-a-file-into-the-python-repl.md)
- [Look Inside Pytest tmp_path](python/look-inside-pytest-tmp-path.md)
- [Make Dataclass Sortable By Specific Field](python/make-dataclass-sortable-by-specific-field.md)
- [Override The Boolean Context Of A Class](python/override-the-boolean-context-of-a-class.md) - [Override The Boolean Context Of A Class](python/override-the-boolean-context-of-a-class.md)
- [Parse Relative Time To datetime Object](python/parse-relative-time-to-datetime-object.md)
- [Reclassify Certain Packages As Dev Dependencies](python/reclassify-certain-packages-as-dev-dependencies.md)
- [Skip Specific Pytest Test Cases](python/skip-specific-pytest-test-cases.md)
- [Sort A List Of Dataclass Instances](python/sort-a-list-of-dataclass-instances.md)
- [Sort Normalized Version Of Data](python/sort-normalized-version-of-data.md)
- [Start The Debugger When A Test Errors](python/start-the-debugger-when-a-test-errors.md)
- [Store And Access Immutable Data In A Tuple](python/store-and-access-immutable-data-in-a-tuple.md) - [Store And Access Immutable Data In A Tuple](python/store-and-access-immutable-data-in-a-tuple.md)
- [Test A Function With Pytest](python/test-a-function-with-pytest.md) - [Test A Function With Pytest](python/test-a-function-with-pytest.md)
- [Use pipx To Install End User Apps](python/use-pipx-to-install-end-user-apps.md) - [Use pipx To Install End User Apps](python/use-pipx-to-install-end-user-apps.md)
- [Use `__post_init__` For `dataclass` Validations](python/use-post-init-for-dataclass-validations.md)
- [Use Verbose Flag To Get More Diff](python/use-verbose-flag-to-get-more-diff.md)
### Rails ### Rails
@@ -1062,9 +1118,12 @@ If you've learned something here, support my efforts writing daily TILs by
- [Cast Common Boolean-Like Values To Booleans](rails/cast-common-boolean-like-values-to-booleans.md) - [Cast Common Boolean-Like Values To Booleans](rails/cast-common-boolean-like-values-to-booleans.md)
- [Change The Nullability Of A Column](rails/change-the-nullability-of-a-column.md) - [Change The Nullability Of A Column](rails/change-the-nullability-of-a-column.md)
- [Change The Time Zone Offset Of A DateTime Object](rails/change-the-time-zone-offset-of-a-datetime-object.md) - [Change The Time Zone Offset Of A DateTime Object](rails/change-the-time-zone-offset-of-a-datetime-object.md)
- [Check How Database Is Configured](rails/check-how-database-is-configured.md)
- [Check If ActiveRecord Update Fails](rails/check-if-activerecord-update-fails.md) - [Check If ActiveRecord Update Fails](rails/check-if-activerecord-update-fails.md)
- [Check If Any Records Have A Null Value](rails/check-if-any-records-have-a-null-value.md) - [Check If Any Records Have A Null Value](rails/check-if-any-records-have-a-null-value.md)
- [Check Specific Attributes On ActiveRecord Array](rails/check-specific-attributes-on-activerecord-array.md) - [Check Specific Attributes On ActiveRecord Array](rails/check-specific-attributes-on-activerecord-array.md)
- [Check The Current Named Log Level](rails/check-the-current-named-log-level.md)
- [Clean Up Memory Hungry Rails Console Processes](rails/clean-up-memory-hungry-rails-console-processes.md)
- [Code Statistics For An Application](rails/code-statistics-for-an-application.md) - [Code Statistics For An Application](rails/code-statistics-for-an-application.md)
- [Columns With Default Values Are Nil On Create](rails/columns-with-default-values-are-nil-on-create.md) - [Columns With Default Values Are Nil On Create](rails/columns-with-default-values-are-nil-on-create.md)
- [Comparing DateTimes Down To Second Precision](rails/comparing-datetimes-down-to-second-precision.md) - [Comparing DateTimes Down To Second Precision](rails/comparing-datetimes-down-to-second-precision.md)
@@ -1371,6 +1430,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [Defaulting To Frozen String Literals](ruby/defaulting-to-frozen-string-literals.md) - [Defaulting To Frozen String Literals](ruby/defaulting-to-frozen-string-literals.md)
- [Define A Custom RSpec Matcher](ruby/define-a-custom-rspec-matcher.md) - [Define A Custom RSpec Matcher](ruby/define-a-custom-rspec-matcher.md)
- [Define A Method On A Struct](ruby/define-a-method-on-a-struct.md) - [Define A Method On A Struct](ruby/define-a-method-on-a-struct.md)
- [Define A Set Of Class Methods](ruby/define-a-set-of-class-methods.md)
- [Define Multiline Strings With Heredocs](ruby/define-multiline-strings-with-heredocs.md) - [Define Multiline Strings With Heredocs](ruby/define-multiline-strings-with-heredocs.md)
- [Destructure The First Item From An Array](ruby/destructure-the-first-item-from-an-array.md) - [Destructure The First Item From An Array](ruby/destructure-the-first-item-from-an-array.md)
- [Destructuring Arrays In Blocks](ruby/destructuring-arrays-in-blocks.md) - [Destructuring Arrays In Blocks](ruby/destructuring-arrays-in-blocks.md)
@@ -1391,6 +1451,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [FactoryGirl Sequences](ruby/factory-girl-sequences.md) - [FactoryGirl Sequences](ruby/factory-girl-sequences.md)
- [Fail](ruby/fail.md) - [Fail](ruby/fail.md)
- [Fetch Warns About Superseding Block Argument](ruby/fetch-warns-about-superseding-block-argument.md) - [Fetch Warns About Superseding Block Argument](ruby/fetch-warns-about-superseding-block-argument.md)
- [Filter By Type](ruby/filter-by-type.md)
- [Find The Min And Max With A Single Call](ruby/find-the-min-and-max-with-a-single-call.md) - [Find The Min And Max With A Single Call](ruby/find-the-min-and-max-with-a-single-call.md)
- [Finding The Source of Ruby Methods](ruby/finding-the-source-of-ruby-methods.md) - [Finding The Source of Ruby Methods](ruby/finding-the-source-of-ruby-methods.md)
- [Format A Hash Into A String Template](ruby/format-a-hash-into-a-string-template.md) - [Format A Hash Into A String Template](ruby/format-a-hash-into-a-string-template.md)
@@ -1418,6 +1479,8 @@ If you've learned something here, support my efforts writing daily TILs by
- [Limit Split](ruby/limit-split.md) - [Limit Split](ruby/limit-split.md)
- [List The Running Ruby Version](ruby/list-the-running-ruby-version.md) - [List The Running Ruby Version](ruby/list-the-running-ruby-version.md)
- [Listing Local Variables](ruby/listing-local-variables.md) - [Listing Local Variables](ruby/listing-local-variables.md)
- [Load A Module And Execute A Statement](ruby/load-a-module-and-execute-a-statement.md)
- [Make A Long String Of Text Readable](ruby/make-a-long-string-of-text-readable.md)
- [Make An Executable Ruby Script](ruby/make-an-executable-ruby-script.md) - [Make An Executable Ruby Script](ruby/make-an-executable-ruby-script.md)
- [Make Structs Easier To Use With Keyword Initialization](ruby/make-structs-easier-to-use-with-keyword-initialization.md) - [Make Structs Easier To Use With Keyword Initialization](ruby/make-structs-easier-to-use-with-keyword-initialization.md)
- [Map With Index Over An Array](ruby/map-with-index-over-an-array.md) - [Map With Index Over An Array](ruby/map-with-index-over-an-array.md)
@@ -1471,6 +1534,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [Single And Double Quoted String Notation](ruby/single-and-double-quoted-string-notation.md) - [Single And Double Quoted String Notation](ruby/single-and-double-quoted-string-notation.md)
- [Skip Specific CVEs When Auditing Your Bundle](ruby/skip-specific-cves-when-auditing-your-bundle.md) - [Skip Specific CVEs When Auditing Your Bundle](ruby/skip-specific-cves-when-auditing-your-bundle.md)
- [Skip The Front Of An Array With Drop](ruby/skip-the-front-of-an-array-with-drop.md) - [Skip The Front Of An Array With Drop](ruby/skip-the-front-of-an-array-with-drop.md)
- [Specify Default For Data Definition](ruby/specify-default-for-data-definition.md)
- [Specify Dependencies For A Rake Task](ruby/specify-dependencies-for-a-rake-task.md) - [Specify Dependencies For A Rake Task](ruby/specify-dependencies-for-a-rake-task.md)
- [Specify How Random Array#sample Is](ruby/specify-how-random-array-sample-is.md) - [Specify How Random Array#sample Is](ruby/specify-how-random-array-sample-is.md)
- [Split A Float Into Its Integer And Decimal](ruby/split-a-float-into-its-integer-and-decimal.md) - [Split A Float Into Its Integer And Decimal](ruby/split-a-float-into-its-integer-and-decimal.md)
@@ -1534,6 +1598,7 @@ If you've learned something here, support my efforts writing daily TILs by
### Taskfile ### Taskfile
- [Add Default Task To List All Tasks](taskfile/add-default-task-to-list-all-tasks.md)
- [Create Interactive Picker For Set Of Subtasks](taskfile/create-interactive-picker-for-set-of-subtasks.md) - [Create Interactive Picker For Set Of Subtasks](taskfile/create-interactive-picker-for-set-of-subtasks.md)
- [Run A Task If It Meets Criteria](taskfile/run-a-task-if-it-meets-criteria.md) - [Run A Task If It Meets Criteria](taskfile/run-a-task-if-it-meets-criteria.md)
@@ -1558,6 +1623,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [Kill Other Connections To A Session](tmux/kill-other-connections-to-a-session.md) - [Kill Other Connections To A Session](tmux/kill-other-connections-to-a-session.md)
- [Kill The Current Session](tmux/kill-the-current-session.md) - [Kill The Current Session](tmux/kill-the-current-session.md)
- [List All Key Bindings](tmux/list-all-key-bindings.md) - [List All Key Bindings](tmux/list-all-key-bindings.md)
- [List Processes Running Across All Session](tmux/list-processes-running-across-all-sessions.md)
- [List Sessions](tmux/list-sessions.md) - [List Sessions](tmux/list-sessions.md)
- [Open New Splits To The Current Directory](tmux/open-new-splits-to-the-current-directory.md) - [Open New Splits To The Current Directory](tmux/open-new-splits-to-the-current-directory.md)
- [Open New Window With A Specific Directory](tmux/open-new-window-with-a-specific-directory.md) - [Open New Window With A Specific Directory](tmux/open-new-window-with-a-specific-directory.md)
@@ -1604,7 +1670,9 @@ If you've learned something here, support my efforts writing daily TILs by
### Unix ### Unix
- [All The Environment Variables](unix/all-the-environment-variables.md) - [All The Environment Variables](unix/all-the-environment-variables.md)
- [Apply Successive Filters To Lines In Less](unix/apply-successive-filters-to-lines-in-less.md)
- [Authorize A cURL Request](unix/authorize-a-curl-request.md) - [Authorize A cURL Request](unix/authorize-a-curl-request.md)
- [Browse And Search Help Docs](unix/browse-and-search-help-docs.md)
- [Cat A File With Line Numbers](unix/cat-a-file-with-line-numbers.md) - [Cat A File With Line Numbers](unix/cat-a-file-with-line-numbers.md)
- [Cat Files With Color Using Bat](unix/cat-files-with-color-using-bat.md) - [Cat Files With Color Using Bat](unix/cat-files-with-color-using-bat.md)
- [Change Default Shell For A User](unix/change-default-shell-for-a-user.md) - [Change Default Shell For A User](unix/change-default-shell-for-a-user.md)
@@ -1616,6 +1684,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [Check The Current Working Directory](unix/check-the-current-working-directory.md) - [Check The Current Working Directory](unix/check-the-current-working-directory.md)
- [Check The Installed OpenSSL Version](unix/check-the-installed-openssl-version.md) - [Check The Installed OpenSSL Version](unix/check-the-installed-openssl-version.md)
- [Clear The Screen](unix/clear-the-screen.md) - [Clear The Screen](unix/clear-the-screen.md)
- [Combine All My TILs Into A Single File](unix/combine-all-my-tils-into-a-single-file.md)
- [Command Line Length Limitations](unix/command-line-length-limitations.md) - [Command Line Length Limitations](unix/command-line-length-limitations.md)
- [Compare Two Variables In A Bash Script](unix/compare-two-variables-in-a-bash-script.md) - [Compare Two Variables In A Bash Script](unix/compare-two-variables-in-a-bash-script.md)
- [Configure cd To Behave Like pushd In Zsh](unix/configure-cd-to-behave-like-pushd-in-zsh.md) - [Configure cd To Behave Like pushd In Zsh](unix/configure-cd-to-behave-like-pushd-in-zsh.md)
@@ -1634,9 +1703,11 @@ If you've learned something here, support my efforts writing daily TILs by
- [Curling For Headers](unix/curling-for-headers.md) - [Curling For Headers](unix/curling-for-headers.md)
- [Curling With Basic Auth Credentials](unix/curling-with-basic-auth-credentials.md) - [Curling With Basic Auth Credentials](unix/curling-with-basic-auth-credentials.md)
- [Determine ipv4 And ipv6 Public IP Addresses](unix/determine-ipv4-and-ipv6-public-ip-addresses.md) - [Determine ipv4 And ipv6 Public IP Addresses](unix/determine-ipv4-and-ipv6-public-ip-addresses.md)
- [Diff Two Files In Unified Format](unix/diff-two-files-in-unified-format.md)
- [Different Ways To Generate A v4 UUID](unix/different-ways-to-generate-a-v4-uuid.md) - [Different Ways To Generate A v4 UUID](unix/different-ways-to-generate-a-v4-uuid.md)
- [Display All The Terminal Colors](unix/display-all-the-terminal-colors.md) - [Display All The Terminal Colors](unix/display-all-the-terminal-colors.md)
- [Display Free Disk Space](unix/display-free-disk-space.md) - [Display Free Disk Space](unix/display-free-disk-space.md)
- [Display Line Numbers While Using Less](unix/display-line-numbers-while-using-less.md)
- [Display The Contents Of A Directory As A Tree](unix/display-the-contents-of-a-directory-as-a-tree.md) - [Display The Contents Of A Directory As A Tree](unix/display-the-contents-of-a-directory-as-a-tree.md)
- [Do A Dry Run Of An rsync](unix/do-a-dry-run-of-an-rsync.md) - [Do A Dry Run Of An rsync](unix/do-a-dry-run-of-an-rsync.md)
- [Do Not Overwrite Existing Files](unix/do-not-overwrite-existing-files.md) - [Do Not Overwrite Existing Files](unix/do-not-overwrite-existing-files.md)
@@ -1662,6 +1733,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [Fix Previous Command With fc](unix/fix-previous-command-with-fc.md) - [Fix Previous Command With fc](unix/fix-previous-command-with-fc.md)
- [Fix Shim Path After asdf Upgrade](unix/fix-shim-path-after-asdf-upgrade.md) - [Fix Shim Path After asdf Upgrade](unix/fix-shim-path-after-asdf-upgrade.md)
- [Fix Unlinked Node Binaries With asdf](unix/fix-unlinked-node-binaries-with-asdf.md) - [Fix Unlinked Node Binaries With asdf](unix/fix-unlinked-node-binaries-with-asdf.md)
- [Format And Display Small Amounts Of Columnar Data](unix/format-and-display-small-amounts-of-columnar-data.md)
- [Forward Multiple Ports Over SSH](unix/forward-multiple-ports-over-ssh.md) - [Forward Multiple Ports Over SSH](unix/forward-multiple-ports-over-ssh.md)
- [Generate A SAML Key And Certificate Pair](unix/generate-a-saml-key-and-certificate-pair.md) - [Generate A SAML Key And Certificate Pair](unix/generate-a-saml-key-and-certificate-pair.md)
- [Generate A Sequence Of Numbered Items](unix/generate-a-sequence-of-numbered-items.md) - [Generate A Sequence Of Numbered Items](unix/generate-a-sequence-of-numbered-items.md)
@@ -1684,6 +1756,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [Ignore A Directory During ripgrep Search](unix/ignore-a-directory-during-ripgrep-search.md) - [Ignore A Directory During ripgrep Search](unix/ignore-a-directory-during-ripgrep-search.md)
- [Ignore The Alias When Running A Command](unix/ignore-the-alias-when-running-a-command.md) - [Ignore The Alias When Running A Command](unix/ignore-the-alias-when-running-a-command.md)
- [Include Ignore Files In Ripgrep Search](unix/include-ignore-files-in-ripgrep-search.md) - [Include Ignore Files In Ripgrep Search](unix/include-ignore-files-in-ripgrep-search.md)
- [Inspect EXIF Data For An Image File](unix/inspect-exif-data-for-an-image-file.md)
- [Interactively Browse Available Node Versions](unix/interactively-browse-availabile-node-versions.md) - [Interactively Browse Available Node Versions](unix/interactively-browse-availabile-node-versions.md)
- [Interactively Switch asdf Package Versions](unix/interactively-switch-asdf-package-versions.md) - [Interactively Switch asdf Package Versions](unix/interactively-switch-asdf-package-versions.md)
- [Interpret Cron Schedule From The CLI](unix/interpret-cron-schedule-from-the-cli.md) - [Interpret Cron Schedule From The CLI](unix/interpret-cron-schedule-from-the-cli.md)
@@ -1730,6 +1803,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [Rename A Bunch Of Files By Constructing mv Commands](unix/rename-a-bunch-of-files-by-constructing-mv-commands.md) - [Rename A Bunch Of Files By Constructing mv Commands](unix/rename-a-bunch-of-files-by-constructing-mv-commands.md)
- [Repeat Yourself](unix/repeat-yourself.md) - [Repeat Yourself](unix/repeat-yourself.md)
- [Replace Pattern Across Many Files In A Project](unix/replace-pattern-across-many-files-in-a-project.md) - [Replace Pattern Across Many Files In A Project](unix/replace-pattern-across-many-files-in-a-project.md)
- [Reverse Each Line Of A File](unix/reverse-each-line-of-a-file.md)
- [Run A Command Repeatedly Several Times](unix/run-a-command-repeatedly-several-times.md) - [Run A Command Repeatedly Several Times](unix/run-a-command-repeatedly-several-times.md)
- [Run A cURL Command Without The Progress Meter](unix/run-a-curl-command-without-the-progress-meter.md) - [Run A cURL Command Without The Progress Meter](unix/run-a-curl-command-without-the-progress-meter.md)
- [Safely Edit The Sudoers File With Vim](unix/safely-edit-the-sudoers-file-with-vim.md) - [Safely Edit The Sudoers File With Vim](unix/safely-edit-the-sudoers-file-with-vim.md)
@@ -1745,6 +1819,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [Show A File Preview When Searching With FZF](unix/show-a-file-preview-when-searching-with-fzf.md) - [Show A File Preview When Searching With FZF](unix/show-a-file-preview-when-searching-with-fzf.md)
- [Show Disk Usage For The Current Directory](unix/show-disk-usage-for-the-current-directory.md) - [Show Disk Usage For The Current Directory](unix/show-disk-usage-for-the-current-directory.md)
- [Show The Size Of Everything In A Directory](unix/show-the-size-of-everything-in-a-directory.md) - [Show The Size Of Everything In A Directory](unix/show-the-size-of-everything-in-a-directory.md)
- [Show Tree View Of Processes And Subprocesses](unix/show-tree-view-of-processes-and-subprocesses.md)
- [Skip Paging If Output Fits On Screen With Less](unix/skip-paging-if-output-fits-on-screen-with-less.md) - [Skip Paging If Output Fits On Screen With Less](unix/skip-paging-if-output-fits-on-screen-with-less.md)
- [SSH Escape Sequences](unix/ssh-escape-sequences.md) - [SSH Escape Sequences](unix/ssh-escape-sequences.md)
- [SSH With Port Forwarding](unix/ssh-with-port-forwarding.md) - [SSH With Port Forwarding](unix/ssh-with-port-forwarding.md)
@@ -1760,7 +1835,9 @@ If you've learned something here, support my efforts writing daily TILs by
- [Unrestrict Where ripgrep Searches](unix/unrestrict-where-ripgrep-searches.md) - [Unrestrict Where ripgrep Searches](unix/unrestrict-where-ripgrep-searches.md)
- [Update Package Versions Known By asdf Plugin](unix/update-package-versions-known-by-asdf-plugin.md) - [Update Package Versions Known By asdf Plugin](unix/update-package-versions-known-by-asdf-plugin.md)
- [Use fzf To Change Directories](unix/use-fzf-to-change-directories.md) - [Use fzf To Change Directories](unix/use-fzf-to-change-directories.md)
- [Use Negative Lookbehind Matching With ripgrep](unix/use-negative-lookbehind-matching-with-ripgrep.md)
- [Use Regex Pattern Matching With Grep](unix/use-regex-pattern-matching-with-grep.md) - [Use Regex Pattern Matching With Grep](unix/use-regex-pattern-matching-with-grep.md)
- [Use The Readline Keybindings Anywhere](unix/use-the-readline-keybindings-anywhere.md)
- [View A Web Page In The Terminal](unix/view-a-web-page-in-the-terminal.md) - [View A Web Page In The Terminal](unix/view-a-web-page-in-the-terminal.md)
- [View The Source For A Brew Formula](unix/view-the-source-for-a-brew-formula.md) - [View The Source For A Brew Formula](unix/view-the-source-for-a-brew-formula.md)
- [Watch The Difference](unix/watch-the-difference.md) - [Watch The Difference](unix/watch-the-difference.md)
@@ -1997,6 +2074,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [Toggle Between Stories In Storybook](workflow/toggle-between-stories-in-storybook.md) - [Toggle Between Stories In Storybook](workflow/toggle-between-stories-in-storybook.md)
- [Update asdf Plugins With Latest Package Versions](workflow/update-asdf-plugins-with-latest-package-versions.md) - [Update asdf Plugins With Latest Package Versions](workflow/update-asdf-plugins-with-latest-package-versions.md)
- [View A Nicely-Formatted CSV In Terminal](workflow/view-a-nicely-formatted-csv-in-terminal.md) - [View A Nicely-Formatted CSV In Terminal](workflow/view-a-nicely-formatted-csv-in-terminal.md)
- [View Nicely Formatted Markdown From Terminal](workflow/view-nicely-formatted-markdown-from-terminal.md)
- [View The PR For The Current GitHub Branch](workflow/view-the-pr-for-the-current-github-branch.md) - [View The PR For The Current GitHub Branch](workflow/view-the-pr-for-the-current-github-branch.md)
### XState ### XState
@@ -2031,6 +2109,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [Add To The Path Via Path Array](zsh/add-to-the-path-via-path-array.md) - [Add To The Path Via Path Array](zsh/add-to-the-path-via-path-array.md)
- [Create And Jump Into A Directory](zsh/create-and-jump-into-a-directory.md) - [Create And Jump Into A Directory](zsh/create-and-jump-into-a-directory.md)
- [Link A Scalar To An Array](zsh/link-a-scalar-to-an-array.md) - [Link A Scalar To An Array](zsh/link-a-scalar-to-an-array.md)
- [List Available Zle Keybindings](zsh/list-available-zle-keybindings.md)
- [Use A Space To Exclude Command From History](zsh/use-a-space-to-exclude-command-from-history.md) - [Use A Space To Exclude Command From History](zsh/use-a-space-to-exclude-command-from-history.md)
## Usage ## Usage
@@ -2056,7 +2135,7 @@ I shamelessly stole this idea from
## License ## License
&copy; 2015-2025 Josh Branchaud &copy; 2015-2026 Josh Branchaud
This repository is licensed under the MIT license. See `LICENSE` for This repository is licensed under the MIT license. See `LICENSE` for
details. details.
+1 -7
View File
@@ -32,8 +32,7 @@ tasks:
notes:sync: notes:sync:
desc: Sync latest changes from the notes submodule desc: Sync latest changes from the notes submodule
cmds: cmds:
- git submodule update --remote {{.NOTES_DIR}} - cd {{.NOTES_DIR}} && git checkout main && git pull
- cd {{.NOTES_DIR}} && git checkout main
silent: false silent: false
notes:open: notes:open:
@@ -61,11 +60,6 @@ tasks:
cmds: cmds:
- git status - git status
notes:pull:
desc: Pull latest changes (alias for sync)
cmds:
- task notes:sync
notes:diff: notes:diff:
desc: Show uncommitted changes in notes desc: Show uncommitted changes in notes
dir: '{{.NOTES_DIR}}' dir: '{{.NOTES_DIR}}'
+18
View File
@@ -0,0 +1,18 @@
# Edit The Current Command Prompt
A neat feature of `bash` is the ability to open whatever the current state of
the command prompt is into your default editor.
Let's say we have a really long command that we've just tried to run, but it
failed and we need to make a small change somewhere in the middle. Instead of
holding the left arrow key for 30 seconds, we can instead hit `CTRL-X CTRL-E`.
This pops us into our `EDITOR` (or maybe `VISUAL`, not sure which). In my case,
that is `nvim`. I now have access to all the features I'm used to in `nvim` for
quickly navigating to and editing, searching and replacing, or whatever.
Once I've got the command how I like it, I can save and exit (`:wq`) and the
updated command will be executed.
This is similar to [the `fc` builtin](unix/fix-previous-command-with-fc.md),
which also happens to be available for `zsh`.
+25
View File
@@ -0,0 +1,25 @@
# Install From Nonstandard Brewfile
When you want to install the packages listed in the `Brewfile` for your current
project (or dotfiles), you can run:
```bash
$ brew bundle
```
And `brew` knows to look for and use the `Brewfile` in the current directory.
If, however, you are trying to run `brew bundle` for a `Brewfile` located
somewhere besides the current directory *OR* you want to target a file with a
non-standard name (like
[`Brewfile.personal`](https://github.com/jbranchaud/dotfiles/blob/main/Brewfile.personal)),
then you can use the `--file` flag.
```bash
$ brew bundle --file Brewfile.personal
```
This is what I do [here in my `dotfiles`
repo](https://github.com/jbranchaud/dotfiles/blob/b053f6251cae7ed52f698fc2a2c40ba82c5881b0/installer/mac-setup.sh#L42-L48).
See `man brew` and find the section on `brew bundle` for more details.
+35
View File
@@ -0,0 +1,35 @@
# Allow Edits From The Start
A common pattern for me when using Claude Code is that I start it up in a
project, I prompt it with a question or feature spec, it either comes up with a
plan or just starts working, and as soon as it is ready to make its first edits
to a file, it prompts me something like:
```
Do you want to make this edit to Taskfile.yml?
1. Yes
2. Yes, allow all edits during this session (shift+tab)
3. Type here to tell Claude what to do differently
```
That's a nice default so that I don't get surprised by Claude Code editing a
bunch of files.
However, if I'm in a git-backed project and I'm going into a session intending
to make edits, then I can skip the formalities. I can tell Claude Code when
starting up the session that edits are allowed.
```sh
$ claude --permission-mode acceptEdits
```
When I do this, I'll see the following indicator below the prompt input field:
```
⏵⏵ accept edits on (shift+tab to cycle)
```
If I've already started `claude` but I forgot to specify that permission mode, I
can also toggle right into _accept edits_ by hitting `Shift+Tab`.
[source](https://www.youtube.com/watch?v=_IK18goX4X8)
+22
View File
@@ -0,0 +1,22 @@
# Resume Specific Session
There are a few different ways to resume a [Claude
Code](https://code.claude.com/docs/en/overview) session.
First, if I have exited a session for the current project and I want to pick
back up with that most recent one, then I can use `claude --continue`.
If I have had a few recent sessions for the current project and I want to
remember what they were and pick up where I left off with one of them, then I
can use `claude --resume` (with no argument). That will open a picker where I
can browser through a summary of the recent sessions based on their starting
prompt. The one I pick is the session that will be resumed.
Finally, if I have grabbed a specific session ID (UUID) during the session from
the `/status` output, then I can reference that value directly.
```sh
$ claude --resume 92170532-be31-4a91-b2a9-025b8fa78232
```
See `claude --help` for more details.
@@ -0,0 +1,25 @@
# Stash The Current Prompt To Send Another First
I've been working my way through the current cohort of Matt Pocock's [Claude
Code for Real
Engineers](https://www.aihero.dev/cohorts/claude-code-for-real-engineers-2026-04).
The best part about going through a series of videos like this is being able to
pick up big and small tips and tricks from another person's workflow.
One of the small things I picked up in an early video is the ability to stash
the current prompt.
Let's say I've gone to the trouble of writing out a detailed prompt, `@`'ing
some files, and so forth. Then I realize I need first prompt Claude to do
something else first. Instead of copy-pasting that prompt into my notes,
deleting it, issuing a different prompt, and then pasting it back in, I can hit
`Ctrl-s`.
`Ctrl-s` will _stash_ the current prompt, clearing out the prompt input. I can
then type in something else. Once I hit enter for that new prompt, it will be
sent to Claude and the stashed prompt will be immediately populated back into
the input.
Though `Ctrl-s` is mentioned when you hit `?` from within `claude` session, I
don't see it documented anywhere in their [Interactive Mode
reference](https://code.claude.com/docs/en/interactive-mode).
@@ -0,0 +1,29 @@
# Set, Get, And Unset Env Vars With Dokku
The `dokku` CLI provides `config` subcommands for managing environment variables
for the target container.
An env var can be set for an active container with `config:set`:
```bash
$ dokku config:set app-name JEMALLOC_ENABLED=true MALLOC_CONF="stats_print:true"
```
Notice I'm able to set multiple env vars at once if needed.
If I ever need to check what an env var is currently set to for one of my app
containers, I can use `config:get`:
```bash
$ dokku config:get app-name JEMALLOC_ENABLED
true
```
I can always override any value with another `config:set`. However, if I need to
entirely remove the env var, I can use `config:unset`:
```bash
$ dokku config:unset app-name MALLOC_CONF
```
[source](https://dokku.com/docs/configuration/environment-variables/)
@@ -0,0 +1,39 @@
# Determine Absolute Path Of Top-Level Project Directory
The `git rev-parse` command is a git plumbing command for parsing different
kinds of things in git into a canonical form that can be used in a deterministic
way by scripts. I would typically think of using it to work with branch names,
tags, and other kinds of refs.
There is a handy, sorta off-label use for it in determining the absolute path of
the root directory for the current git repository. Use the `--show-toplevel`
flag with no other arguments.
```bash
git rev-parse --show-toplevel
/Users/lastword/dev/jbranchaud/til
```
Here, I am in the local copy of [my TIL repo](https://github.com/jbranchaud/til). This command gives me the absolute
path of the top-level directory where that `.git` directory resides.
This is useful for scripts that need to orient themselves to the current
project's top-level directory regardless of what directory they are being
executed from. This is useful for things like a git hook script or monorepos
with scripts located in a specific sub-project directory.
Also worth mentioning is the `--show-superproject-working-tree` flag. In my TIL
repo, I have a private repository included as a submodule. Within that directory
`--show-toplevel` will produce the absolute path to the submodule. If I instead
want the absolute path of the _super project_ (in this case TIL), then I can use
this other flag.
```bash
git rev-parse --show-toplevel
/Users/lastword/dev/jbranchaud/til/notes
git rev-parse --show-superproject-working-tree
/Users/lastword/dev/jbranchaud/til
```
See `man git-rev-parse` for more details.
@@ -0,0 +1,30 @@
# Display All Git Log Entries In My Local Timezone
I tend to work with remote teams distributed across across multiple time zones.
In that context, it is important to have an awareness of what time zone each
person is operating in and to communicate clearly around that.
When looking at the output for `git log` on a distributed team, the timestamps
for each entry can be all over the place. If I want to understand when something
was committed, I have to look at the time as well as the time zone offset and
mentally translate it to my own time zone.
There is a `git config` option to alleviate this issue by having `git log`
convert and display all timestamps into your local time zone.
```bash
$ git config --global log.date rfc-local
```
Running that will add this entry to your _global_ git config file:
```
[log]
date = rfc-local
```
Now the time that was displaying as `Wed Apr 8 20:12:33 2026 -0400` will display
as `Wed, 8 Apr 2026 19:12:33 -0500`.
This also helps with smoothing out differences from DST and for commits produced
by AI agents in sandbox environments where the locale is set to UTC.
@@ -0,0 +1,33 @@
# Programmatically Grab SHA For Head Commit
When I use `gh browse path/to/some-file.txt`, it opens the browser to that file
in GitHub. However, it targets the default branch (`main`) by default which is
not very useful as a permalink because what that file looks like on `main` is
liable to change.
There is a `--commit` flag you can use to have it instead open to that file at a
specific commit SHA.
So what SHA do I pass as an argument to that flag?
Often what I would like to grab is a reference to the current version of the
file which is whatever it looks like for the `HEAD` commit. But `HEAD` is
another moving target reference. The `git rev-parse` command can translate
`HEAD` into a specific SHA though.
```bash
git rev-parse --short HEAD
3402428
git rev-parse HEAD
3402428aadc02cfdc9825c8feb593443e72f50cd
```
Either of those will work. I can use a bash command substitution then to tie it
all together into a single command:
```bash
gh browse path/to/some-file.txt --commit=$(git rev-parse --short HEAD)
```
See `man git-rev-parse` for more details.
+33
View File
@@ -0,0 +1,33 @@
# Skip Git Hooks As Needed
Projects have Git hooks configured for all sorts of reasons. Most common are
`pre-commit` hooks which verify certain aspects of the contents of a commit.
A `pre-commit` hook could check that the tests all pass, that the changes don't
include any debugging statements, and so forth. There are all kinds of hooks
though, like `pre-rebase` and `post-checkout`.
These hooks can sometimes get in the way and we may need to skip or disable them
on a one-off basis.
Several Git commands offer a `--no-verify` flag which can skip running the hook
associated with that command.
- `git commit --no-verify` (skips `pre-commit` and `commit-msg` hooks)
- `git push --no-verify` (skips `pre-push` hook)
- `git merge --no-verify` (skips `pre-merge-commit` hook)
- `git am --no-verify` (skips `applypatch-msg` and `pre-applypatch` hooks)
If you look in the `.git/hooks` directory, there are several other hooks not
covered by the above. So, what if I am doing an action like `git checkout` and I
want to skip the `post-checkout` hook?
I can override the `hooksPath` config for that one command with the `-c` flag.
```sh
$ git -c core.hooksPath=/dev/null checkout ...
```
By setting it to `/dev/null`, it will find *no* hooks available, so none will be
executed for this command.
See `man git-config` for more details on `core.hooksPath`.
@@ -0,0 +1,36 @@
# Undo Latest Changes Committed To Specific File
I'm reviewing the changes I've made in a PR before I request a review from my
team. There are a scattering of changes in one file that I've changed my mind
on. Everything else looks good though. So, I need to undo the changes in that
file before proceeding.
Manually undoing them is going to be clunky. There is a way to do it with `git
checkout`, but that is one of the ways in which `git-checkout` was overloaded
leading to the release of `git-restore`.
Let's use `git-restore` instead. By specifying a `--source`, I can tell `git`
what _ref_ in the commit history that file should be restored to. I'm on a
short-lived feature branch, so pointing to `main` is good enough.
```bash
$ git restore --source=main app/models/customer.rb
```
If I've changed a file at multiple points on this feature branch and I don't
want to undo all of them, then pointing to `main` is no longer going to work.
Instead, I can point to the commit right before the current one (`HEAD`) that
I'm trying to undo.
```bash
$ git restore --source=HEAD~ app/models/customer.rb
```
This really isn't much different than the `git-checkout` version, but I still
find it to be a little clearer.
```bash
$ git checkout HEAD~ -- app/models/customer.rb
```
See `man git-restore` for more details.
+31
View File
@@ -0,0 +1,31 @@
# List PRs Awaiting Your Review
If you work on a software team or steward an open-source project, then there are
likely some open PRs that you've been tagged to review. I am usually able to
catch most review requests as they come up either from the GitHub email
notifications or by keeping an eye on the PRs tab of active projects. Sometimes
I get consumed by a task and something slips through the cracks.
There are a couple other ways to quickly check if anything is waiting on my
review.
From the web UI I can visit the following URL which will show all PRs across all
projects where my review has been requested:
[https://github.com/pulls/review-requested](https://github.com/pulls/review-requested)
The GitHub CLI (`gh`) can do the same and I can do it right from the terminal
instead of navigating several clicks within GitHub's web UI.
```bash
$ gh search prs --review-requested=@me --state=open
```
That too will list PRs across all projects that are open and awaiting my review.
If that one ends up being a little too noisy, you can also use `gh` to _list_
just PRs for the current project:
```bash
$ gh pr list --search "review-requested:@me"
```
@@ -0,0 +1,46 @@
# Open File To Specific Line In Browser
Often one of the best ways to point a teammate to a line of code is to share a
GitHub link to a specific file and line number. Sometimes even a specific
commit.
For the longest time I would manually open GitHub, navigate to that file, and so
forth. The `gh` CLI supports this with the `browse` subcommand and it takes way
less time if you already have the repo in your local filesystem.
For instance, if I want to point you to line 11 of the `zshrc.local` file in my
`dotfiles` repo, I can run the following command:
```bash
$ gh browse zshrc.local:11
```
That would open a browser tab to
[https://github.com/jbranchaud/dotfiles/blob/main/zshrc.local?plain=1#L11](https://github.com/jbranchaud/dotfiles/blob/main/zshrc.local?plain=1#L11).
If I wanted a range of lines, I could change it from `11` to, say, `11-27`:
```bash
$ gh browse zshrc.local:11-27
```
And I would see this in the browser --
[https://github.com/jbranchaud/dotfiles/blob/main/zshrc.local?plain=1#L11-L27](https://github.com/jbranchaud/dotfiles/blob/main/zshrc.local?plain=1#L11-L27).
Both of these URLs are pointing to the `main` branch. If I instead want to
reference a specific commit, I can use the `--commit` flag.
```bash
$ gh browse zshrc.local:11-27 --commit=f2f9e78d4fc784643f725c88f7a5a7a077e7f261
```
I grabbed that from the latest commit in `git log`. That opens to
[https://github.com/jbranchaud/dotfiles/blob/f2f9e78d4fc784643f725c88f7a5a7a077e7f261/zshrc.local?plain=1#L11-L27](https://github.com/jbranchaud/dotfiles/blob/f2f9e78d4fc784643f725c88f7a5a7a077e7f261/zshrc.local?plain=1#L11-L27).
Another way of doing that would be to use `git rev-parse HEAD`:
```bash
$ gh browse zshrc.local:11-27 --commit=$(git rev-parse HEAD)
```
See `gh browse --help` for more details.
@@ -0,0 +1,32 @@
# Check Ruby Version For Production App
While deploying a fresh Rails app to Heroku recently, I ran into an issue. The
`it` block argument wasn't working despite being on Ruby 4.0. Or so I thought.
Running the following command reported the Ruby version of that Heroku server
instance:
```bash
heroku run -- ruby --version
Running ruby --version on ⬢ my-app... up, run.3090
ruby 3.3.9 (2025-07-24 revision f5c772fc7c) [x86_64-linux]
```
I was on `3.3.9` which must have been the fallback default at the time.
Though I had set the Ruby version in my `.ruby-version` file, I had neglected to
specify it in the `Gemfile` as well. Once I added it to the `Gemfile` and
redeployed, my Heroku server instance was running the expected version of Ruby.
```bash
heroku run -- ruby --version
Running ruby --version on ⬢ my-app... up, run.5353
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [x86_64-linux]
```
Note: because [I have set `HEROKU_ORGANIZATION` and
`HEROKU_APP`](set-default-team-and-app-for-project.md) in my environment
(`.envrc`) for the local copy of the app, I don't need to specify those when
running the `heroku run` command above.
See `heroku run --help` for more details.
@@ -0,0 +1,12 @@
# Hide Overflowing Text For Google Sheets Column
I imported a big CSV into a new Google Sheets document. This included a
"Description" column with many of the descriptions varying between 50 and 80
characters. The bottom line is that the description column was flowing over the
top of the columns next to it. Instead of expanding the width of that column as
far as the largest description, I wanted to hide the _overflow_.
The way to do this in Google Sheets is to highlight the entire column by
clicking on the column grouping. Then under the _Format_ menu item is a
_Wrapping_ submenu. The _Clip_ option is what I was looking for because it clips
the text that gets shown at the edge of the column.
+26
View File
@@ -0,0 +1,26 @@
# Count Number Of Tokens In A File
Over time you have accumulated a bunch of small directives, corrections, and
project details in your `CLAUDE.md` or `AGENTS.md` file. The file doesn't seem
too big, but you are mindful that it is being included in every prompt. How many
tokens is it eating from the context window?
OpenAI's BPE (Byte Pair Encoding) tokenization library,
[`tiktoken`](https://github.com/openai/tiktoken), is an open-source Python
package. If it is installed on our machine, then we can use it as part of the
following one-liner to check a file:
```bash
python -c "import tiktoken, sys; print(len(tiktoken.encoding_for_model('gpt-4o').encode(open(sys.argv[1], 'r', encoding='utf-8').read())))" \
AGENTS.md
1018
```
I ran this against the `AGENTS.md` file in a team project I'm on. It came out to
1018 tokens. This is a very good approximation based on the tokenizer trained
for `gpt-4o`. The tokenizers may vary a little from model to model, but the
differences for our purposes here are going to be negligible.
This one-liner gets the "first" argument to the command, reads it in, and runs
that string against the tokenizer. The length of the tokenized encoding is then
printed.
@@ -0,0 +1,21 @@
# Control Which Monitor App Switcher Appears On
For the most part when I hit `cmd+tab` (and `cmd+shift+tab`) to switch between
apps, the visual switcher UI (which shows a row of the open apps) appears on my
main monitor. However, sometimes I will be hitting `cmd+tab` and nothing shows
up on my main monitor. I look to the right at my side monitor and there is the
app switcher UI.
Why is it appearing over there all of a sudden?
The reason is that the app switcher UI is anchored to the same screen where the
doc is located. Though the doc defaults to my main monitor, if I access the doc
from the side monitor, now it is anchored there.
To switch it back, I just have to make the doc slide up on my main monitor by
running my mouse down to the bottom of that screen.
The switch up was because I accidentally accessed the doc on my side monitor
without realizing.
[source](https://superuser.com/a/744680)
@@ -0,0 +1,20 @@
# Reveal Location Of File In Finder.app
In the terminal I have the path to an image file. I want to open Finder.app to
the location of that image file so that I can drag and drop it into a file
upload area in the browser.
Instead of opening a Finder.app window and navigating directory by directory to
the location, I can use the `open` command. Using `open` directly with the image
file will open the image in Preview.app. I want to reveal the directory that the
image file is in within Finder.app. _Reveal_ is the keyword and the `-R` flag
does just that.
Here is an example of this that I actually ran when uploading a screenshot that
went into [this blogmark post](https://still.visualmode.dev/blogmarks/255):
```bash
$ open -R /Users/lastword/images/tiobe-index-graph-march-2026.png
```
See `man open` for more details.
@@ -1,8 +1,7 @@
# Create Umbrella Task For All Test Tasks # Create Umbrella Task For All Test Tasks
When I was first sketching out the [`mise` When I was first sketching out the [`mise` tasks](https://mise.jdx.dev/tasks/running-tasks.html) for a Rails app, I added
tasks](https://mise.jdx.dev/tasks/running-tasks.html) for a Rails app, I added the following two tasks. One is for running all the `rspec` tests. The other is
the following two tasks. One is for running all the `rspec` tests. The Other is
for running all the `vitest` (JavaScript) tests. for running all the `vitest` (JavaScript) tests.
```toml ```toml
@@ -49,5 +48,4 @@ Running `mise run test:all` won't execute its own command, but because it
depends on all other `test:*` tasks, the tests will get run through those depends on all other `test:*` tasks, the tests will get run through those
dependencies. dependencies.
This task naming pattern also allows for calling all tests with `mise run This task naming pattern also allows for calling all tests with `mise run "test:**"`.
"test:**"`.
+27
View File
@@ -0,0 +1,27 @@
# Look In Ruby Version Dotfile
Newer versions of [`mise`](https://mise.jdx.dev/dev-tools/) specifically only
look for tool versions in `mise.toml` as well as the asdf `.tool-versions` file.
A lot of Ruby projects use the `.ruby-version` file to indicate the Ruby version
of a project. To continue to use the `.ruby-version` file instead of migrating
to `mise.toml`, you need to tell `mise` that you prefer to use the idiomatic
version file.
I added the following line to my
[`~/.config/mise/config.toml`](https://github.com/jbranchaud/dotfiles/commit/8edeb7a9c53500e89e88b4079cbd1859ebebcbda)
file:
```toml
idiomatic_version_file_enable_tools = ["ruby"]
```
Now, whenever `mise` is looking for the specified Ruby version of a project, it
will also look for `.ruby-version`.
Here is a [full list of idomatic version files supported by
`mise`](https://mise.jdx.dev/configuration.html#idiomatic-version-files).
See
[`idiomatic_version_file_enable_tools`](https://mise.jdx.dev/configuration/settings.html#idiomatic_version_file_enable_tools)
as well as the [Ruby-specific documentation](https://mise.jdx.dev/lang/ruby.html#ruby-version-and-gemfile-support)
for more details.
+37
View File
@@ -0,0 +1,37 @@
# Override Your Project Mise File
A project I'm working on has a version-controlled `.mise.toml` file in it. Some
changes were made to that recently that introduce some env vars that conflict
with my setup. If I make edits to that file, then I have a modified version of
`.mise.toml` sitting in my Git working copy.
```
# .mise.toml
[env]
CONFIG_SETTING = "project"
```
Instead, I can rely on the loading precedence rules of `mise` to override those
project settings with my individual settings. I can do that with the
`.mise.local.toml` file which is played on top of any `mise` configuration from
files further down the precedence chain.
```
# .mise.local.toml
[env]
CONFIG_SETTING = "override"
```
Assuming I have `mise` setup with my shell environment to automatically load in
these files, I can now check what takes precedence:
```bash
$ echo $CONFIG_SETTING
override
```
Make sure `.mise.local.toml` is included in the `.gitignore` file to avoid
checking in your personal environment overrides.
To be sure about what files are loaded and in what order, give `mise cfg` a try.
I discuss that in more detail in [List The Files Being Loaded By Mise](list-the-files-being-loaded-by-mise.md).
@@ -0,0 +1,38 @@
# Pick From Tasks Using Interactive Picker
In [Add Mise Tasks For Common Workflow
Commands](https://www.visualmode.dev/add-mise-tasks-for-common-workflow-commands),
I wrote about a set of tasks I added as shortcuts for connecting to the `rails console` in various environments.
```toml
# mise.toml
[tasks."console:staging"]
description = "Open a Rails console on staging"
run = "ssh -t my-app-staging dokku run my-app rails console"
[tasks."console:prod"]
description = "Open a Rails console on production"
run = "ssh -t my-app-prod dokku run my-app rails console"
```
When a project is configured with multiple `mise` tasks like this, we can invoke
`mise run` without any specific arguments and it will prompt you with an
interactive picker. The picker will populate with all the tasks like so:
```bash
mise run
Tasks
Select a task to run
console:prod Open a Rails console on production
console:staging Open a Rails console on staging
/
esc clear filter • enter confirm
```
We can navigate between the options with the arrow keys (and if we exit _filter_
mode by hitting `esc`, then `j/k` also work to move down and up). While in
_filter_ mode, we can type into the prompt which will filter the list of
commands down to just the partial matches.
Once we're targeting the task we want to run, we hit `enter` and the task is
executed.
@@ -0,0 +1,29 @@
# Search Through Bin Paths For Tool Locations
The `mise bin-paths` command will list all the bin paths that are managed by
`mise`. When you tell `mise` to install a tool, it installs a specific version
at a location where its binaries can be made accessible on the system path.
While `mise ls` is useful for seeing what is installed by `mise` and at what
version, the `bin-paths` command can tell you where those tool installations
with their binaries are located.
Combine this with `grep` or `rg` to narrow down the results to tools by a
specific name:
```bash
mise bin-paths | rg 'neovim'
/Users/lastword/.local/share/mise/installs/npm-neovim/5.4.0/bin
/Users/lastword/.local/share/mise/installs/pipx-neovim-remote/2.5.1/bin
/Users/lastword/.local/share/mise/installs/neovim/0.11.6/bin
```
I can then look in one of these directories to see the one or more binaries that
they include. For instance, here is what is in the `node` bin path:
```bash
ls /Users/lastword/.local/share/mise/installs/node/22.22.0/bin
 ./  ../  claude@  corepack@  node*  npm*  npx@
```
See `mise bin-paths --help` for more details.
@@ -0,0 +1,29 @@
# Avoid Vulnerabilities In New Package Versions
It seems like every week there is a new supply chain attack where malicious code
is embedded in a popular, widely-used OSS package. This week's is
[axios](https://www.stepsecurity.io/blog/axios-compromised-on-npm-malicious-versions-drop-remote-access-trojan).
The [`pnpm` package manager](https://pnpm.io/) has a nice feature that helps
avoid installing these vulnerable package versions in the first place.
> To reduce the risk of installing compromised packages, you can delay the
> installation of newly published versions. In most cases, malicious releases
> are discovered and removed from the registry within an hour.
The [`minimumReleaseAge` config option](https://pnpm.io/settings#minimumreleaseage) tells `pnpm` to not install
a dependency (including transitive ones) until it has been released for at least
that many minutes.
For instance, if you wanted to set this to 72 hours, then you'd set this option
to `4320` minutes like so:
```
$ pnpm config set minimum-release-age 4320 -g
```
The global flag (`-g`) will set that in your global config location, e.g.
`$XDG_CONFIG_HOME/pnpm/rc`. You could also add it specifically to your project
in the `pnpm-workspace.yaml` file.
[source](https://bsky.app/profile/styfle.dev/post/3miekuyeyrs2w)
@@ -0,0 +1,44 @@
# Compute Median Instead Of Average
One of the first aggregate functions we might use in PostgreSQL, besides `sum`,
is `avg`.
```sql
select avg(book_count) as average_books_read
from (
select users.id, count(books.id) as book_count
from users
left join books
on books.user_id = users.id
where books.read_in_year = 2025
group by users.id
) as user_book_counts;
```
This computes the average of the set of values which sums them all up
and divides by the count. The average (maybe you've heard this also called the
_mean_) is not always the best way to understand data, especially when there are
outliers.
Instead, we might want to compute the _median_ value of our set of data. There
is no easily identifiable `median` aggregate function. Instead, we can use
`percentile_cont` with a value of `0.5`. This gets us the 50th percentile of our
set of data which is the definition of the _median_.
```sql
select percentile_cont(0.5) within group (
order by book_count
) as median_books_read
from (
select users.id, count(books.id) as book_count
from users
left join books on books.user_id = users.id and books.read_in_year = 2025
group by users.id
) as user_book_counts;
```
The full syntax for `percentile_cont` is `percentile_cong(precision) within
group (order by ...)` because this is an aggregiate that has to work with an
ordered-set of data.
[source](https://www.postgresql.org/docs/current/functions-aggregate.html)
@@ -0,0 +1,34 @@
# Access Most Recent Return Value In REPL
One of my favorite features of Ruby's `irb` and `pry` are that you can use `_`
to reference the most recent return value. Often as we use an interpreter or
REPL, we end up with _intermediate_ values. That is, we've execute some kind of
statement which returned a value and we now want to use that resulting value in
our next statement. Python also supports `_`.
Let's say I've run a statement that took a while to process, but I forgot to
assign it to a variable. Instead of re-running the whole thing, I can create a
variable that references the previous return value using `_`.
```python
>>> BytePairEncoding.train_bpe(long_text)
{'merge_rules': [...], 'vocab': {...}}
>>> result = _
>>> list(result.keys())
['merge_rules', 'vocab']
```
Even if I don't necessarily want to assign it a variable, it can be nice to
reference the previous value as I continue with what I'm doing:
```python
>>> result['merge_rules'][0][1]
256
>>> result['vocab'][_]
b'e '
```
Notice how the value from the first statement gets used as part of a `dict`
access.
[source](https://docs.python.org/3/tutorial/introduction.html#numbers)
@@ -0,0 +1,44 @@
# Access Variables Outside Loop Scope
Here is a function that loops over a list to find the first occurrence of a
falsy value.
```python
def find_false(self):
for item in self.items:
item_type = type(item)
print(f"Current item: {item} ({item_type})")
if not item:
break
print(f"First false item: {item} ({item_type})")
```
Notice how at the end of the function, outside of the loop, I am able to access
both `item` (defined in the loop definition) and `item_type` (defined within the
loop's body).
Both of these variables are defined, by the loop, in _function scope_ and are
accessible anywhere in the function after they have been defined.
The title of this TIL is a bit of a misnomer because Python doesn't have the
concept of a _loop scope_. There are two levels of scope in Python --
module/global scope and function scope.
I spend most of my time writing Ruby which also has _block scope_, so Python's
simplified two-level scoping took me by surprise.
Though the code sample above is contrived, this function scope assignment can be
taken advantage of with loop definitions in scenarios where you want to know
what the last `item` defined was before the loop terminated.
```python
for submission in submissions:
if passes(submission, criteria):
break
else:
raise ValueError("No submissions that meet given criteria")
print(f"Submit first passing submission: {submission.id}")
submit(submission)
```
@@ -0,0 +1,38 @@
# Assert Is Only A Development Check
The `assert` keyword is used in Python to write a statement that will check some
assertion and raise an error if it isn't met. This is only meant to be used as a
check during development because it can be easily optimized out of the code.
```python
stuff = None
assert stuff, "We need to have some stuff to proceed"
print(f"We have {stuff or 'something'}!")
```
If I execute this code with `python`, it will raise on that second line of code.
```bash
python assert_example.py
Traceback (most recent call last):
File "/Users/lastword/dev/jbranchaud/py-vmt/assert_example.py", line 3, in <module>
assert stuff, "We need to have some stuff to proceed"
^^^^^
AssertionError: We need to have some stuff to proceed
```
This `assert` statement will be stripped out of the compiled bytecode if the
`-O` (capital o) flag is used. Notice how running the same file with that flag
does not lead to an `AssertionError`.
```python
python -O assert_example.py
We have something!
```
If I want to make sanity checks for situations that would be caused by a bug in
the code, an `assert` statement can be a good candidate. However, if I am making
runtime checks like validating user input, then an `if` statement and raising
something like a `ValueError` is better.
@@ -0,0 +1,47 @@
# Avoid Modification With Frozen Dataclass
The `@dataclass` decorator can be set as _frozen_ to prevent modification of
values on instances of that `dataclass`.
Without making it frozen, I can easily subvert validations by changing the value
of attributes after the `__post_init__` validations are called.
```python
>>> config = BPEConfig(300, []) # passes validations
>>> config.vocab_size = 22 # this is invalid, wish this was prevented
```
Here is the updated `@dataclass` declaration with `frozen=True` passed as a
parameter.
```python
from dataclasses import dataclass
from typing import ClassVar
@dataclass(frozen=True)
class BPEConfig:
BASE_VOCAB_SIZE: ClassVar[int] = 256
vocab_size: int
special_tokens: list[str]
def __post_init__(self):
if self.vocab_size < self.BASE_VOCAB_SIZE:
msg = f"vocab_size ({self.vocab_size}) must be greater than or equal to BASE_VOCAB_SIZE ({self.BASE_VOCAB_SIZE})"
raise ValueError(msg)
```
Now I am prevented from modifying a scalar value like `vocab_size` after the
instance has been created.
```python
>>> config = BPEConfig(300, [])
>>> config.vocab_size = 22
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 4, in __setattr__
dataclasses.FrozenInstanceError: cannot assign to field 'vocab_size'
```
This doesn't prevent you from modifying the contents of attributes that are
`list` or `dict` types.
@@ -0,0 +1,50 @@
# Check If Package Is Installed With Pip
I recently installed PyTorch, but when I tried using it, I was getting an error
about `numpy` not being installed. I was kind of surprised by that because I
thought I would have already had that.
I wanted to check, so I asked with `pip show`:
```bash
python3 -m pip show numpy
WARNING: Package(s) not found: numpy
```
I can even list everything that is installed with `pip` using `pip list` like
so:
```bash
python3 -m pip list
Package Version Build
------------------ --------- -----
certifi 2026.1.4
cffi 2.0.0
charset-normalizer 3.4.4
click 8.3.1
commonmark 0.9.1
cryptography 46.0.3
docutils 0.22.4
filelock 3.24.2
fsspec 2026.2.0
idna 3.11
Jinja2 3.1.6
...
```
I then installed `numpy` (`python3 -m pip install numpy`) and how I can use `pip
show` again to confirm that.
```bash
python3 -m pip show numpy
Name: numpy
Version: 2.4.2
Summary: Fundamental package for array computing in Python
Home-page: https://numpy.org
Author: Travis E. Oliphant et al.
Author-email:
License-Expression: BSD-3-Clause AND 0BSD AND MIT AND Zlib AND CC0-1.0
Location: /Users/lastword/.local/share/mise/installs/python/3.12.12/lib/python3.12/site-packages
Requires:
Required-by:
```
@@ -0,0 +1,40 @@
# Control Passing Of Time In Tests
While it is nice to be able to write pure functional code, our software still
lives in the real world and may have to relate to or depend on the passing of
time. In order to test this kind of code, we need time to behave in a reliable,
deterministic way. One of the best ways to create a testing environment where
that is true is to bring in tooling that hijacks time.
The [`freezegun` module](https://github.com/spulec/freezegun) is a great tool
for that job. We can use it to freeze time at a specific testable point, advance
time a specific amount, and much more.
Here is an example from the tests for [my CLI-based time tracking
app](https://github.com/jbranchaud/py-vmt/blob/acb26e4840279d936a12f16c505ca7e75e9a6d20/tests/src/py_vmt/test_cli.py#L21)
where I freeze time before starting a session. That gives me a chance to assert
about the exact start time that is output by the command. Then I can advance
time a little and assert that the `status` command outputs the correct thing.
```python
import datetime
from freezegun import freeze_time
# some other test setup omitted ...
initial_datetime = datetime.datetime(
2026, 3, 14, 15, 5, 11, 0, datetime.timezone.utc
)
with freeze_time(initial_datetime) as frozen_datetime:
# start a session
start_result = runner.invoke(cli, ["start", "my-project"])
output = "Started tracking 'my-project' at 10:05AM"
assert output in start_result.output
frozen_datetime.tick(delta=datetime.timedelta(minutes=30))
# check status
status_result = runner.invoke(cli, ["status"])
output = "Tracking 'my-project' for 30m (since 10:05AM)"
assert output in status_result.output
```
@@ -0,0 +1,44 @@
# Create A Range Of Descending Values
A typical use of `range` looks something like this:
```python
>>> list(range(1, 5))
[1, 2, 3, 4]
```
Which is equivalent to this one where we give a `step` value of `1`.
```python
>>> list(range(1, 5, 1))
[1, 2, 3, 4]
```
If we try to create a _negative range_, that is, a range of values in decreasing
order, we get an empty list.
```python
>>> list(range(0, -7))
[]
```
That's because the `step` value still defaults to `1`. And there are no positive
steps between `0` and `-7`. So, let's give `range` a `step` value of `-1`.
```python
>>> list(range(0,-7, -1))
[0, -1, -2, -3, -4, -5, -6]
```
One practical use case of a negative range like this is using a list
comprehension to transform it into a list of the _last seven days_.
```python
>>> from datetime import datetime, timedelta
>>> [datetime.now().date() + timedelta(days=days) for days in range(0,-7, -1)]
[datetime.date(2026, 3, 19), datetime.date(2026, 3, 18), datetime.date(2026, 3, 17), datetime.date(2026, 3, 16), datetime.date(2026, 3, 15), datetime.date(2026, 3, 14), datetime.date(2026, 3, 13)]
```
Of course this could have been written with a positive range and then
subtracting the `timedelta`. I like that I have the option of doing this in
whatever way makes the code most readable.
+46
View File
@@ -0,0 +1,46 @@
# Deduplicate A List Into A Tuple
A `list` is not hashable which means you can't use it for things like `dict`
keys or cache keys. Instead you need to convert it into something like a `set`
or a `tuple`.
Here is an example list:
```python
>>> l1 = [3,4,1,2,5,4,1]
```
Turning this list into a `set` or `frozenset` is straightforward:
```python
>>> set(l1)
{1, 2, 3, 4, 5}
>>> frozenset(l1)
frozenset({1, 2, 3, 4, 5})
```
If you're trying to preserve the order after deduplicating, then you'll want to
use a `tuple` instead of a `set`. In order to deduplicate while maintaining the
ordering, you can exploit the fact that `dict` keys maintain their order. A
`list` can be transformed into the keys of a `dict` with
[`dict.fromkeys`](https://docs.python.org/3/library/stdtypes.html#dict.fromkeys):
```python
>>> dict.fromkeys(l1)
{3: None, 4: None, 1: None, 2: None, 5: None}
```
And here is your `tuple` which extracts the keys of the `dict`:
```python
>>> tuple(dict.fromkeys(l1))
(3, 4, 1, 2, 5)
```
By comparison, here is the `tuple` transformed directly from the `list` without
deduplication.
```python
>>> tuple(l1)
(3, 4, 1, 2, 5, 4, 1)
```
@@ -0,0 +1,60 @@
# Define Sequence Of Tests With Parametrize Decorator
I have a function that I want to test across a bunch of different inputs. That
way I can make sure the logic of that function handles all the different
scenarios I have in mind.
While working on [`py-vmt`](https://github.com/jbranchaud/py-vmt), I started by
writing a big single test function with a sequence of variable assignments and
`assert` statements. Here's my starting point:
```python
def test_format_time_delta_everything():
# less than a minute
thirty_seconds = timedelta(seconds=30)
assert "30s" == format_time_delta(thirty_seconds)
# one minute exactly
one_minute = timedelta(seconds=60)
assert "1m" == format_time_delta(one_minute)
# more than a minute
assert "1m30s" == format_time_delta(one_minute + thirty_seconds)
# bunch of minutes and seconds
delta = timedelta(minutes=24, seconds=8)
assert "24m8s" == format_time_delta(delta)
# one hour exactly
one_hour = timedelta(hours=1)
assert "1h" == format_time_delta(one_hour)
# more than one hour
assert "1h24m" == format_time_delta(one_hour + delta)
```
I knew I would eventually need to break it up into individual test functions,
but I couldn't bare to start there because it seemed quite repetitive.
There is another way to approach this without all the duplication. Pytest comes
with [a "parametrize" decorator](https://docs.pytest.org/en/stable/example/parametrize.html). This is
used to define a set of test data (and expected values) that will get passed
one-by-one to the test function as parameters.
```python
@pytest.mark.parametrize("input,expected", [
(timedelta(seconds=30), "30s"),
(timedelta(seconds=60), "1m"),
(timedelta(seconds=90), "1m30s"),
(timedelta(minutes=24, seconds=8), "24m8s"),
(timedelta(hours=1), "1h"),
(timedelta(hours=1, minutes=24, seconds=8), "1h24m"),
])
def test_format_time_delta(input, expected):
assert format_time_delta(input) == expected
```
I ditch all of the duplication this way. I define a list of tuples that
represent my input values and expected values. Then the body of the test can be
minimal. And I get a separate test execution for each parameter tuple making it
easier to see fine-grained pass/fail results.
@@ -0,0 +1,53 @@
# Easy Key-Value Aggregates With defaultdict
The `collections` module has the `defaultdict` object that can be used to
aggregate values tied to a key. What sets this apart from simply using a `dict`
is that we get the base value for free. So if our aggregate value is a list,
then we get `[]` by default for each new key. In the same way, we'd get `0` if
it was constructed with `int`.
Here is the counter example from [Keep A Tally With
collections.Counter](keep-a-tally-with-collections-counter.md)
```python
from collections import defaultdict
def get_pair_counts(token_ids: list[int]) -> Counter:
"""Count how often each adjacent pair appears"""
counts = defaultdict(int)
for i in range(len(token_ids) - 1):
pair = (token_ids[i], token_ids[i + 1])
counts[pair] += 1
return counts
```
We never have to initially set a key to `0`. If the key is not yet present, then
`int()` (the zero-value constructor) is used as the `__missing__` value.
We can do the same with `list`:
```python
>>> import collections
>>> stuff = collections.defaultdict(list)
>>> stuff['alpha'].append(1)
>>> stuff['alpha']
[1]
>>> stuff['beta']
[]
```
In the same way, this uses `list()` as the `__missing__` value to start of each
key with an `[]`.
I find this so handy because in other languages I've typically had to do
something more like this:
```python
words_by_length = {}
for item in items:
if len(item) not in words_by_length:
words_by_length[len(item)] = []
words_by_length[len(item)].append(item)
```
This is much clunkier.
@@ -0,0 +1,64 @@
# Get Absolute Seconds From `timedelta` Object
The [`timedelta` object provided by
`datetime`](https://docs.python.org/3/library/datetime.html#timedelta-objects)
is a useful built-in concept for representing a duration of time.
```python
>>> from datetime import timedelta
>>> diff = timedelta(hours=1, minutes=1, seconds=6)
>>> diff.seconds
3666
```
It is pretty minimal though. There are only a couple things you can inspect
about it -- `days`, `seconds` (as I did in the snippet above), and
`microseconds`.
And perhaps that is enough to hint at the issue I recently ran into with it --
specifically that you can access both `days` and `seconds`.
Let's look at what happens when I have a `timedelta` with more than a day worth
of seconds.
```python
>>> diff = timedelta(seconds=(3600 * 24 + 1))
>>> diff.seconds
1
>>> diff.days
1
```
I thought `seconds` was going to produce `86401` instead of `1`. The reason is
because any amount of duration over a day gets converted into the `days` value
and its the remaining time smaller than a day that is represented by `seconds`.
In my [original implementation of
`format_time_delta`](https://github.com/jbranchaud/py-vmt/blob/c14eaa56cf5f5c6d0120a95f04f95a6c87443e1c/src/py_vmt/time_helpers.py#L11-L14),
I was trying to build a relative time string by converting `seconds` into hours,
minutes, and seconds. That approach falls apart as soon as the delta is greater
than a day.
```python
def format_time_delta(diff) -> str:
hours, remainder = divmod(diff.seconds, 3600)
minutes, remainder = divmod(remainder, 60)
seconds = remainder
# ...
```
Instead, I needed to reach for [the `total_seconds()` function](https://docs.python.org/3/library/datetime.html#datetime.timedelta.total_seconds).
This gives "the total number of seconds contained in the duration" and is
described as equivalent to `diff / timedelta(seconds=1)`.
Here is the [updated version of `format_time_delta`](https://github.com/jbranchaud/py-vmt/blob/ec1875a9d73552f5481e3945ddf522e94d0cc018/src/py_vmt/time_helpers.py?plain=1#L11-L16):
```python
def format_time_delta(diff: timedelta) -> str:
total_seconds = int(diff.total_seconds())
hours, remainder = divmod(total_seconds, 3600)
minutes, remainder = divmod(remainder, 60)
seconds = remainder
```
@@ -0,0 +1,42 @@
# Get Quotient And Remainder In One Operation
While writing some custom code to transform a number of seconds into the
constituent hours, minutes, and seconds, I found myself needing to get both the
quotient and remainder from a division between two numbers.
```python
>>> import math
>>> math.floor(3666 / 3600)
1
>>> 3666 % 3600
66
```
Instead, I can use Python's built-in
[`divmod`](https://docs.python.org/3/library/functions.html#divmod) function to
compute both values in one statement.
```python
>>> divmod(3666, 3600)
(1, 66)
```
The result is a tuple with the first value being my quotient (in this case, the
number of hours) and the remainder (the remaining number of seconds).
This kind of operation is known as [Euclidian
Division](https://en.wikipedia.org/wiki/Euclidean_division).
Here is a snippet of some actual code where I use this in
[`py-vmt`](https://github.com/jbranchaud/py-vmt/blob/b9eae8b258e9fd720cfa3bb63b601225df352051/src/py_vmt/time_helpers.py#L14-L16):
```python
def format_time_delta(diff: timedelta) -> str:
total_seconds = int(diff.total_seconds())
hours, remainder = divmod(total_seconds, 3600)
minutes, remainder = divmod(remainder, 60)
seconds = remainder
# ...
```
@@ -0,0 +1,17 @@
# Install With PIP For Specific Interpreter
The `pip` module can be invoked for any of its commands, such as install, using
a specific Python interpreter like so:
```bash
$ python3 -m pip install black
```
This avoid ambiguity between the version of Python I am using and version of the
package manager I'm using.
Similarly if I need to upgrade `pip`, I can do the following:
```bash
$ python3 -m pip install --upgrade pip
```
@@ -0,0 +1,27 @@
# Iterate First N Items From Enumerable
As I'm working through the 2nd chapter of [Build a Large Language Model (from
scratch)](https://still.visualmode.dev/blogmarks/227), I came across a code
example processing a dictionary of words. This example used a for loop to print
out each dictionary entry until an index of 50 was reached on then it did a
`break`.
This struck me as an odd way to grab and process N items from a list. I did some
searching and found `itertools` which provides
[`islice`](https://docs.python.org/3/library/itertools.html#itertools.islice).
```python
from itertools import islice
# preprocess words from a file into a word list
all_words = ... # not shown here
vocab = {token: integer for integer, token in enumerate(all_words)}
for item in islice(enumerate(vocab.items()), 50):
print(item)
```
The `islice` function is a better approach because the intention (to grab the
first 50 things) is encoded in the function call rather than buried in a loop
body. It also has equivalent memory efficiency to the original example because
it lazily processes the list of `vocab` items.
+34
View File
@@ -0,0 +1,34 @@
# Iterate Over A Dictionary
Let's say we have a `dict` that contains counts of occurrences for each word in
some sample text:
```python
words_frequency = {
"the": 4,
"a": 3,
"dog": 1,
"bone": 1,
"wants": 1,
...
}
```
Here is how we can iterate over the `dict`, accessing both the keys and values:
```python
for word, count in word_frequency.items():
print(f"- {word} appears {count} time{'' if count == 1 else 's'}")
```
Using the
[`items()`](https://docs.python.org/3/library/stdtypes.html#dict.items) method,
we're able to access both _key_ and _value_ with the for loop as it iterates.
Another approach is to loop directly on the `dict` which implicitly surfaces the
_key_ for iteration. This can then be used to get the value from the `dict`:
```python
for word in word_frequency:
print(f"- {word}: {word_frequency[word]}
```
@@ -0,0 +1,40 @@
# Keep A Tally With collections.Counter
Python's `collections` module comes with a
[`Counter`](https://docs.python.org/3/library/collections.html#collections.Counter)
object which is a specialized dict subclass focussed on tallying counts of keys.
> It is a collection where elements are stored as dictionary keys and their
> counts are stored as dictionary values. Counts are allowed to be any integer
> value including zero or negative counts.
I used it recently while doing an exploratory implementation of a Byte-Pair
Encoding (BPE):
```python
from collections import Counter
def get_pair_counts(token_ids: list[int]) -> Counter:
"""Count how often each adjacent pair appears"""
counts = Counter()
for i in range(len(token_ids) - 1):
pair = (token_ids[i], token_ids[i + 1])
counts[pair] += 1
return counts
```
Here I'm able to count the number of occurrences of each pair of bytes from the
input text. A tuple of `int` values is hashable, so they work great as keys for
a `Counter`.
The count value of any key will default to `0`. That makes it straightforward to
increment from there as you iterating over occurrences.
```python
>>> counts = Counter()
>>> counts['hello']
0
>>> count['hello'] += 1
>>> count['hello']
1
```
@@ -0,0 +1,35 @@
# Load A File Into The Python REPL
I opened up a Python REPL to try some things out.
```
$ python3
>>> import math
>>> math.floor(5/2)
2
```
Now, I want to reference a Python file I've been working on so that I can
manually test the behavior of what I'm building. To do this, I can import a file
by its name in the same way that I would import any module. Then I can use that
namespace for class and method references. Crucially, the file should exist in
the same directory the REPL was started from.
First, here is the file:
```python
# bpe.py
class BytePairEncoding:
def text_to_bytes(text: str) -> list[int]:
"""Convert a string to a list of byte values (0-255)"""
return list(text.encode("utf-8"))
```
Now to use it from the REPL:
```
$ python
>>> import bpe
>>> bpe.BytePairEncoding.text_to_bytes("Gimme some bytes!")
[71, 105, 109, 109, 101, 32, 115, 111, 109, 101, 32, 98, 121, 116, 101, 115, 33]
```
+41
View File
@@ -0,0 +1,41 @@
# Look Inside Pytest tmp_path
In [Isolate and Debug File Side-Effects with Pytest
`tmp_path`](https://www.visualmode.dev/isolate-and-debug-file-side-effects-with-pytest-tmp-path),
I wrote about how I use
[`tmp_path`](https://docs.pytest.org/en/stable/reference/reference.html#std-fixture-tmp_path)
in a Pytest fixture to test [my `py-vmt` CLI](https://github.com/jbranchaud/py-vmt). During testing of the CLI interface
via [`click`'s testing utilities](https://click.palletsprojects.com/en/stable/testing/), `vmt` creates,
modifies, and reads from files. Isolating that behavior with the `tmp_path`
fixture is useful because it prevents individual test cases from conflicting
with one another.
Here is what the fixture looks like at the top of my test file:
```python
# auto fixture for all test cases that monkeypatches the platform dirs to a tmp
# path so that test side-effects don't persist between runs
@pytest.fixture(autouse=True)
def use_tmp_platform_dirs(tmp_path, monkeypatch):
data_dir = tmp_path / "data"
config_dir = tmp_path / "config"
data_dir.mkdir()
config_dir.mkdir()
monkeypatch.setattr(CliContext, "get_data_dir", staticmethod(lambda: data_dir))
monkeypatch.setattr(CliContext, "get_config_dir", staticmethod(lambda: config_dir))
```
The root of the temp directory is located at `tempfile.gettempdir()` and the
directories from there are organized with this structure:
```
{temproot}/pytest-of-{user}/pytest-{num}/{testname}/
```
So, in the case of `vmt`, I can find the `config` and `data` dirs for a specific
test run here:
```bash
ls /var/folders/zc/q6gnvbgx6kq77828jn38716r0000gn/T/pytest-of-lastword/pytest-2/test_start_status_stop_flow0
config data
```
@@ -0,0 +1,45 @@
# Make Dataclass Sortable By Specific Field
One way to sort a list of some `dataclass` is to define the `key` parameter when
calling `sort` or `sorted` like I discussed in [Sort a List of Dataclass
Instances](sort-a-list-of-dataclass-instances.md):
```python
for date in sessions_grouped_by_day.keys():
sessions_grouped_by_day[date].sort(
key=lambda session: session.start_time.time()
)
```
But then that lambda for `key` needs to be defined everywhere you sort.
If the dataclass has a single, specific field that acts as a natural proxy for
sort order, then you can define that in the `dataclass` implementation with the
`__lt__` method.
As long as a class defines the _less than_ dunder method, it will be sortable.
Here is what that looks like for this `Session` dataclass:
```python
from dataclasses import dataclass
from datetime import datetime, timezone
@dataclass
class Session:
start_time: datetime
project_name: str
end_time: datetime | None = None
def __lt__(self, other):
if not isinstance(other, Session):
return NotImplemented
return self.start_time < other.start_time
# more methods below ...
```
This implementation of `__lt__` tells the sorting methods that _this_ (`self`)
instance of `Session` can be compared to some `other` instance of `Session` by
comparing their `start_time` values to see which is less than. The guard at the
beginning makes sure only instances of `Session` are being compared.
@@ -0,0 +1,62 @@
# Parse Relative Time To datetime Object
I was looking for an out-of-the-box solution for parsing natural language,
relative time strings (e.g. `'33 minutes ago'`) into valid `datetime` objects.
The best library for this is
[`dateparser`](https://dateparser.readthedocs.io/en/latest/).
While it is as easy to use this as _import_ then _parse_:
```python
>>> import dateparser
>>> dateparser.parse('33 minutes ago')
datetime.datetime(2026, 3, 7, 23, 19, 9, 17855)
```
There is more to it if we need to deal with timezones.
In my use case, I wanted to my `datetime` object to be timezone-aware and I
wanted to store it in `UTC`.
As is, the above simple `datetime` object is not `tzaware`, meaning it doesn't
have any `tzinfo` attached to it.
```python
>>> dateparser.parse('33 minutes ago').tzinfo is not None
False
```
We need to pass some additional settings during `parse`.
```python
>>> settings = {'RETURN_AS_TIMEZONE_AWARE': True}
>>> dateparser.parse('33 minutes ago', settings=settings)
>>> _
datetime.datetime(2026, 3, 8, 9, 53, 36, 225099, tzinfo=zoneinfo.ZoneInfo(key='America/Chicago'))
>>> settings['TO_TIMEZONE'] = 'UTC'
>>> dateparser.parse('33 minutes ago', settings=settings)
>>> _
datetime.datetime(2026, 3, 8, 14, 54, 47, 34041, tzinfo=<StaticTzInfo 'UTC'>)
```
The first step to getting a `datetime` object that is `tzaware` is to set
`RETURN_AS_TIMEZONE_AWARE` to `True`. That picks up the locale setting of the
system it is running on -- in my case, I'm in Chicago.
I said I wanted to store this as UTC though. That means I need to pass an
additional setting `TO_TIMEZONE` with a value of `'UTC'` which will translate
the `datetime` from my local time to UTC -- notice the 5 hour difference from
`9` to `14`.
Storing `datetime` details like this with timezone info _as_ UTC is nice because
it keeps everything consistent at the storage layer and then at the presentation
layer I can always convert it right back to the local timezone with
`astimezone`.
```python
>>> _.astimezone()
datetime.datetime(2026, 3, 8, 9, 54, 47, 34041, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=68400), 'CDT'))
```
See the [`datetime` docs](https://docs.python.org/3/library/datetime.html) for
more details.
@@ -0,0 +1,76 @@
# Reclassify Certain Packages As Dev Dependencies
When I first started working on [py-vmt](https://github.com/jbranchaud/py-vmt),
I wasn't differentiating certain test packages as _dev_ dependencies as opposed
to standard, production dependencies. This can lead to bloated installs across a
variety of distribution channels.
Notice that I have everything treated as production dependencies:
```bash
uv tree --no-dev
Resolved 18 packages in 2ms
py-vmt v0.1.0
├── click v8.3.1
├── dateparser v1.3.0
│ ├── python-dateutil v2.9.0.post0
│ │ └── six v1.17.0
│ ├── pytz v2026.1.post1
│ ├── regex v2026.2.28
│ └── tzlocal v5.3.1
├── freezegun v1.5.5
│ └── python-dateutil v2.9.0.post0 (*)
├── platformdirs v4.9.4
├── pytest v9.0.2
│ ├── iniconfig v2.3.0
│ ├── packaging v26.0
│ ├── pluggy v1.6.0
│ └── pygments v2.19.2
└── types-dateparser v1.3.0.20260211
(*) Package tree already displayed
```
`pytest`, `freezegun`, and `types-dateparser` are better suited as _dev_
dependencies.
I can reclassify them by moving them from `dependencies` into a `dev` dependency
group in `pyproject.toml`:
```toml
dependencies = ["click>=8.3.1", "dateparser>=1.3.0", "platformdirs>=4.9.4"]
[dependency-groups]
dev = ["freezegun>=1.5.5", "pytest>=9.0.2", "types-dateparser>=1.3.0.20260211"]
```
I only had `dependencies` before, so I had to add `[dependency-groups]` and `dev = []` to my `pyproject.toml` file.
I can then tell `uv` to sync up the installation and virtualenv based on the new
organization of the dependencies.
```bash
uv sync
warning: Skipping installation of entry points (`project.scripts`) because this project is not packaged; to install entry points, set `tool.uv.package = true` or define a `build-system`
Resolved 18 packages in 518ms
```
Now when I check the `--no-dev` tree of dependencies, it's just the essentials:
```bash
uv tree --no-dev
Resolved 18 packages in 1ms
py-vmt v0.1.0
├── click v8.3.1
├── dateparser v1.3.0
│ ├── python-dateutil v2.9.0.post0
│ │ └── six v1.17.0
│ ├── pytz v2026.1.post1
│ ├── regex v2026.2.28
│ └── tzlocal v5.3.1
└── platformdirs v4.9.
```
Another way to achieve this would have been to run `uv remove` and `uv add` with
the relevant sets of package names. In retrospect, I would have preferred using
that approach in the first place. If you're wanting to be pinned to specific
versions of certain packages, you'd have to be a little more careful to get this
right.
+38
View File
@@ -0,0 +1,38 @@
# Skip Specific Pytest Test Cases
While using a failing test case to build a small new feature for
[`py-vmt`](https://github.com/jbranchaud/py-vmt), I realized I needed to do some
refactoring first. It wasn't significant enough to warrant stashing my current
changes and switching to a different branch, so I kept all the changes around. I
did find the initial failing test distracting from the refactoring I was trying
to do. To temporarily shelve that failure, I can use a Pytest decorator to mark
it as _skipped_.
```python
@pytest.mark.skip(reason="not yet implemented")
def test_log_recent_activity():
runner = CliRunner()
# set up the data dir file with some existing session entries
initial_datetime = datetime.datetime(
2026, 3, 14, 15, 5, 11, 0, datetime.timezone.utc
)
with freeze_time(initial_datetime) as frozen_datetime:
# ...
```
The [`@pytest.mark.skip` decorator](https://docs.pytest.org/en/stable/how-to/skipping.html#skipping-test-functions)
tells the Pytest runner to skip of that specific test case instead of executing
it. In the test runner output, I'll see an `s` rather than a `.` or `F` and the
summary will include it in a count of skipped tests:
```
=========================== 3 failed, 4 passed, 1 skipped in 0.09s ===========================
```
Another way to think about this is to mark this test case as _expected to fail_
with `@pytest.mark.xfail`. That will display as an `x` and show up in the summary as:
```
=========================== 3 failed, 4 passed, 1 xfailed in 0.11s ===========================
```
@@ -0,0 +1,52 @@
# Sort A List Of Dataclass Instances
Sorting lists of scalar values (integers, strings, floats, even booleans) in
Python is simple because the natural ordering of the list elements will be used.
We can call `sorted` on the list and it _just works_.
```python
>>> items = ["orange", "apple", "banana", "mango"]
>>> sorted(items)
['apple', 'banana', 'mango', 'orange']
```
However, if we have a list of non-scalar values, it is a little more complex. We
have to give `sorted` some help with knowing how to sort things that don't have
a natural ordering.
Let's take this `dataclass` that represents a time-based `Session` as an
example.
```python
from dataclasses import dataclass
from datetime import datetime, timezone
@dataclass
class Session:
start_time: datetime
project_name: str
end_time: datetime | None = None
# plus several methods ...
```
If I have a list of `Session` instances that I want to sort, I have to give
`sorted` a `key` to sort on. In the case of these `Session` instances, we'll
pass a `lambda` that can be evaluated to determine the sort value (which needs
to be sortable). `datetime` instances are sortable and I want to sort these
sessions based on their `start_time` values.
Here is a snippet from my `py_vmt` CLI where I make sure that each list of
sessions in this day-by-day `dict` is sorted based on the `start_time`:
```python
for date in sessions_grouped_by_day.keys():
sessions_grouped_by_day[date].sort(
key=lambda session: session.start_time.time()
)
```
`sort` (and `sorted`) translates each item in the list to the values produced
by the lambda and then sorts them by those values.
[source](https://docs.python.org/3/howto/sorting.html)
+33
View File
@@ -0,0 +1,33 @@
# Sort Normalized Version Of Data
Let's say I have a list of names that I want to sort. However, because of
inconsistency in how the data was entered, sometimes those names are capitalized
and other times they are not. Using
[`methodcaller`](https://docs.python.org/3/library/operator.html#operator.methodcaller),
I can normalize the sorting `key` used when comparing list items.
First, let's look at calling `sorted` with the list and no `key`:
```python
>>> sorted(["butler", "Jemisin", "le guin", "Erdrich"])
['Erdrich', 'Jemisin', 'butler', 'le guin']
```
`butler` which starts with a `b` gets moved to the 3rd position because it is
lowercase.
To sort this list using a normalized comparison, we will use `methodcaller` to
create a callable out of `lower` which is then passed as the sort `key`:
```python
>>> from operator import methodcaller
>>> sorted(["butler", "Jemisin", "le guin", "Erdrich"], key=methodcaller("lower"))
['butler', 'Erdrich', 'Jemisin', 'le guin']
```
That's the sort order I was originally hoping for.
What `methodcaller` is doing is creating a callable function that will invoke
`lower` with each string instance as the target. Conceptually similar to
`"Erdrich".lower()` or even `getattr("Erdrich", "lower")()` (notice this needs
to be immediately invoked).
@@ -0,0 +1,27 @@
# Start The Debugger When A Test Errors
While working on [some
tests](https://github.com/jbranchaud/build-an-llm-from-scratch/blob/main/tests/chapter_02/test_bpe_tokenizer.py)
for my Byte Pair Encoding tokenizer, I was running into an unexpected test
failure. To better understand what was going on, I needed to inspect the state
of the program around the time the code raised an exception.
Instead of needing to manually set a breakpoint at the correct spot to begin
debugging, I can run the test with the Pytest-supported `--pdb` flag. That's
short for _python debugger_.
> Start the interactive Python debugger on errors or KeyboardInterrupt
What this does during a test run is opens you up to the interactive Python
debugger at the exact moment an exception is raised. This gives you the ability
to inspect values of the program state at that point in execution which could
help inform the needed fix.
```bash
uv run pytest -vv --pdb -k "test_train_bpe"
```
There I am running a specific test that matches against `-k "test_train_bpe"`
and the python debugger will start up if there is an error.
See `uv run pytest --help` for more details.
@@ -0,0 +1,54 @@
# Use `__post_init__` For `dataclass` Validations
The [`dataclass`](https://docs.python.org/3/library/dataclasses.html) construct
is a handy stdlib way of modeling some data with many improvements over a `dict`
such as named attributes and type visibility.
```python
from dataclasses import dataclass
from typing import ClassVar
@dataclass
class BPEConfig:
BASE_VOCAB_SIZE: ClassVar[int] = 256
vocab_size: int
special_tokens: list[str]
```
I want to enhance `BPEConfig` a little by validating the `vocab_size` which
cannot be less than the `BASE_VOCAB_SIZE`. The
[`__post_init__`](https://docs.python.org/3/library/dataclasses.html#dataclasses.__post_init__)
method is a good place for this kind of validation.
```python
from dataclasses import dataclass
from typing import ClassVar
@dataclass
class BPEConfig:
BASE_VOCAB_SIZE: ClassVar[int] = 256
vocab_size: int
special_tokens: list[str]
def __post_init__(self):
if self.vocab_size < self.BASE_VOCAB_SIZE:
msg = f"vocab_size ({self.vocab_size}) must be greater than or equal to BASE_VOCAB_SIZE ({self.BASE_VOCAB_SIZE})"
raise ValueError(msg)
```
With this in place, my program will fail fast if I try to use an invalid
`vocab_size`:
```python
>>> BPEConfig(22, [])
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 5, in __init__
File "/Users/lastword/dev/misc/build-an-llm/chapter_02/bpe_tokenizer.py", line 24, in __post_init__
raise ValueError(msg)
ValueError: vocab_size (22) must be greater than or equal to BASE_VOCAB_SIZE (256)
```
This example is pulled directly from [the `BPETokenizer` I'm building](https://github.com/jbranchaud/build-an-llm-from-scratch/blob/d3fd0acd65c3e7419b2d15a64c8d74266d0488f6/chapter_02/bpe_tokenizer.py#L14-L24).
+161
View File
@@ -0,0 +1,161 @@
# Use Verbose Flag To Get More Diff
Here is the output of running some `pytest` unit tests. A couple of the tests
pass, which produces little output. But I get a big block of details for the one
failing test. In this case the failure is an assertion between two lists that
don't match.
```bash
uv run pytest
========================================== test session starts ==========================================
platform darwin -- Python 3.12.12, pytest-9.0.2, pluggy-1.6.0
rootdir: /Users/lastword/dev/misc/build-an-llm
configfile: pyproject.toml
collected 3 items
tests/chapter_02/test_bpe_tokenizer.py .F. [100%]
=============================================== FAILURES ================================================
_____________________________________ test_merge_with_byte_sequence _____________________________________
def test_merge_with_byte_sequence():
token_ids = [1, 2, 3, 4, 5, 2, 3, 1, 2, 3, 4, 1]
merged_tokens = BPETokenizer._merge(token_ids, [2, 3, 4], 256)
# assert merged_tokens == [1, 256, 5, 2, 3, 1, 256, 1]
> assert merged_tokens == [1, 256, 5, 4, 5, 1, 256, 1]
E assert [1, 256, 5, 2, 3, 1, ...] == [1, 256, 5, 4, 5, 1, ...]
E
E At index 3 diff: 2 != 4
E Use -v to get more diff
tests/chapter_02/test_bpe_tokenizer.py:13: AssertionError
======================================== short test summary info ========================================
FAILED tests/chapter_02/test_bpe_tokenizer.py::test_merge_with_byte_sequence - assert [1, 256, 5, 2, 3, 1, ...] == [1, 256, 5, 4, 5, 1, ...]
====================================== 1 failed, 2 passed in 0.02s ======================================
```
The lists are too long to fully display in the failure output. `pytest` is able
to tell us two useful things though. First, it mentions that the first
discrepancy in the lists is at index `3` where `2 != 4`. Second, it says `Use -v
to get more diff`.
Let's try rerunning the tests with `-v`.
```bash
uv run pytest -v
========================================== test session starts ==========================================
platform darwin -- Python 3.12.12, pytest-9.0.2, pluggy-1.6.0 -- /Users/lastword/dev/misc/build-an-llm/.venv/bin/python3
cachedir: .pytest_cache
rootdir: /Users/lastword/dev/misc/build-an-llm
configfile: pyproject.toml
collected 3 items
tests/chapter_02/test_bpe_tokenizer.py::test_merge_with_byte_pair PASSED [ 33%]
tests/chapter_02/test_bpe_tokenizer.py::test_merge_with_byte_sequence FAILED [ 66%]
tests/chapter_02/test_bpe_tokenizer.py::test_subsequence_at_index PASSED [100%]
=============================================== FAILURES ================================================
_____________________________________ test_merge_with_byte_sequence _____________________________________
def test_merge_with_byte_sequence():
token_ids = [1, 2, 3, 4, 5, 2, 3, 1, 2, 3, 4, 1]
merged_tokens = BPETokenizer._merge(token_ids, [2, 3, 4], 256)
# assert merged_tokens == [1, 256, 5, 2, 3, 1, 256, 1]
> assert merged_tokens == [1, 256, 5, 4, 5, 1, 256, 1]
E AssertionError: assert [1, 256, 5, 2, 3, 1, ...] == [1, 256, 5, 4, 5, 1, ...]
E
E At index 3 diff: 2 != 4
E
E Full diff:
E [
E 1,
E 256,...
E
E ...Full output truncated (13 lines hidden), use '-vv' to show
tests/chapter_02/test_bpe_tokenizer.py:13: AssertionError
======================================== short test summary info ========================================
FAILED tests/chapter_02/test_bpe_tokenizer.py::test_merge_with_byte_sequence - AssertionError: assert [1, 256, 5, 2, 3, 1, ...] == [1, 256, 5, 4, 5, 1, ...]
====================================== 1 failed, 2 passed in 0.02s ======================================
```
That was sort of a tease because it starts to display a "Full diff", but that
gets quickly truncated. `pytest` then tells us that we can `use '-vv' to show`
the full diff.
```bash
uv run pytest -vv
========================================== test session starts ==========================================
platform darwin -- Python 3.12.12, pytest-9.0.2, pluggy-1.6.0 -- /Users/lastword/dev/misc/build-an-llm/.venv/bin/python3
cachedir: .pytest_cache
rootdir: /Users/lastword/dev/misc/build-an-llm
configfile: pyproject.toml
collected 3 items
tests/chapter_02/test_bpe_tokenizer.py::test_merge_with_byte_pair PASSED [ 33%]
tests/chapter_02/test_bpe_tokenizer.py::test_merge_with_byte_sequence FAILED [ 66%]
tests/chapter_02/test_bpe_tokenizer.py::test_subsequence_at_index PASSED [100%]
=============================================== FAILURES ================================================
_____________________________________ test_merge_with_byte_sequence _____________________________________
def test_merge_with_byte_sequence():
token_ids = [1, 2, 3, 4, 5, 2, 3, 1, 2, 3, 4, 1]
merged_tokens = BPETokenizer._merge(token_ids, [2, 3, 4], 256)
# assert merged_tokens == [1, 256, 5, 2, 3, 1, 256, 1]
> assert merged_tokens == [1, 256, 5, 4, 5, 1, 256, 1]
E assert [1, 256, 5, 2, 3, 1, 256, 1] == [1, 256, 5, 4, 5, 1, 256, 1]
E
E At index 3 diff: 2 != 4
E
E Full diff:
E [
E 1,
E 256,
E 5,
E - 4,
E ? ^
E + 2,
E ? ^
E - 5,
E ? ^
E + 3,
E ? ^
E 1,
E 256,
E 1,
E ]
tests/chapter_02/test_bpe_tokenizer.py:13: AssertionError
======================================== short test summary info ========================================
FAILED tests/chapter_02/test_bpe_tokenizer.py::test_merge_with_byte_sequence - assert [1, 256, 5, 2, 3, 1, 256, 1] == [1, 256, 5, 4, 5, 1, 256, 1]
At index 3 diff: 2 != 4
Full diff:
[
1,
256,
5,
- 4,
? ^
+ 2,
? ^
- 5,
? ^
+ 3,
? ^
1,
256,
1,
]
====================================== 1 failed, 2 passed in 0.02s ======================================
```
This is a lot more output to look at. What we can perhaps see more clearly now
is that the lists match up until there is a mismatch between `2` and `4` at the
third index. And then right after that is another mismatch between `3` and `5`.
This kind of output can only scale so much, so use it when it works and when the
diff view starts to fall short, rework the assertions to get more readable and
actionable test output.
+46
View File
@@ -0,0 +1,46 @@
# Check How Database Is Configured
While making some adjustments to the database connection string (`DATABASE_URL`)
for a pre-production Rails environment, we wanted to check that configuration
options like `sslmode` were picked up.
From a `rails console` session I can check the live database configuration like
so:
```ruby
> ActiveRecord::Base.connection_db_config.configuration_hash
=> {
adapter: "postgresql",
encoding: "unicode",
pool: 5,
database: "my_app_development"
}
```
I can look at the
[`configuration_hash`](https://api.rubyonrails.org/classes/ActiveRecord/DatabaseConfigurations/HashConfig.html#attribute-i-configuration_hash)
from `rails console` of my pre-prod environment to see more configuration
settings:
```ruby
> ActiveRecord::Base.connection_db_config.configuration_hash
=> {
adapter: "postgresql",
encoding: "unicode",
pool: 5,
username: "app_user",
password: "super_s3cr3t",
port: 15432,
database: "pre_prod_database",
host: "some-host-123.ondigitalocean.com",
sslmode: "verify-full"
}
```
Since I was specifically looking for the `sslmode` value, I can access that
directly:
```ruby
> ActiveRecord::Base.connection_db_config.configuration_hash[:sslmode]
=> "verify-full"
```
@@ -0,0 +1,46 @@
# Check The Current Named Log Level
I'm connected to a `rails console` session for an active Rails app. I want to
check the current log level.
```ruby
> Rails.logger.level
=> 1
```
The `1` doesn't mean much to me at a glance. I can translate that to the
severity level using the `Logger::SEV_LABLE` constant.
```ruby
[44] pry(main)> Logger::SEV_LABEL[Rails.logger.level]
=> "INFO"
```
Ah yes, `INFO`, that makes sense as the default.
I can see all the severity levels by inspecting the constant itself.
```ruby
[45] pry(main)> Logger::SEV_LABEL
=> ["DEBUG", "INFO", "WARN", "ERROR", "FATAL", "ANY"]
```
As I convenience, I can set the label using the index, the string, or even a
symbol.
```ruby
> Rails.logger.level
=> 1
> Rails.logger.level = "WARN"
=> "WARN"
> Rails.logger.level
=> 2
> Rails.logger.level = :debug
=> :debug
> Rails.logger.level
=> 0
```
See the [Debugging Rails Applications
guide](https://guides.rubyonrails.org/debugging_rails_applications.html#log-levels)
for more details.
@@ -0,0 +1,43 @@
# Clean Up Memory Hungry Rails Console Processes
I noticed (using `htop`) that a remote server hosting a Rails app had most of
its RAM being actively consumed. This was hindering my ability to run a fresh
deploy because the deploy processes had to do a ton of memory swapping which
drastically slowed the whole thing down.
With some investigation, I discovered that most of the memory was being consumed
by a handful of `rails console` processes. I didn't have any known active `rails console` processes that I was using. That combined with the dates of these
processes starting way in the past suggested to me that these were abandoned
processes that hadn't been properly cleaned up.
```bash
server:~# ps aux | grep rails
32767 878915 0.0 0.0 1227160 936 pts/0 Ssl+ 2025 0:03 /exec rails console
32767 878942 0.9 6.5 830996 261748 pts/0 Rl+ 2025 249:51 ruby /app/bin/rails console
32767 3004097 0.0 0.0 1227160 692 pts/0 Ssl+ 2025 0:04 /exec rails console
32767 3004129 0.9 6.4 834672 257228 pts/0 Dl+ 2025 406:31 ruby /app/bin/rails console
32767 3048582 0.0 0.0 1227160 940 pts/0 Ssl+ Jan09 0:00 /exec rails console
32767 3048611 1.1 6.3 829936 253484 pts/0 Dl+ Jan09 60:50 ruby /app/bin/rails console
32767 3060033 0.0 0.0 1227160 944 pts/0 Ssl+ 2025 0:04 /exec rails console
32767 3060063 0.9 6.5 838084 260812 pts/0 Rl+ 2025 405:37 ruby /app/bin/rails console
root 3699372 0.0 0.0 7008 1300 pts/0 S+ 15:51 0:00 grep --color=auto rails
server:~# ps aux | grep 'rails console' | awk '{sum+=$6} END {print sum/1024 " MB"}'
1014.64 MB
```
As we can see by tacking on this `awk` command, these processes are consuming
1GB of memory.
Each of these is a pair of processes. A parent process (`/exec rails console`)
that kicks off and supervises the memory-hungry child process (`ruby /app/bin/rails console`).
To free up this memory, I targeted each of the parent processes with a `kill`
command one by one. For example:
```bash
server:~# kill 878915
```
I suspect that I may have left the occasional terminal tab open with one of
these `rails console` processes running and the SSH connection was getting
killed without the `rails console` getting killed with it.
+46
View File
@@ -0,0 +1,46 @@
# Define A Set Of Class Methods
The most common way to define class methods is by defining them directly with
`self` (the class in the current context) on a method by method basis:
```ruby
class User
def self.find_by(attrs)
# lookup logic ...
end
end
```
If you have a group of class methods you want to define, you can stick them all
within a `class << self` block which does similarly defines each of them as
singleton methods of that class (`User` in this case):
```ruby
class User
class << self
def find_by_email(email)
# lookup logic ...
end
def find_by_last_name(last_name)
# lookup logic ...
end
end
end
```
This opens the singleton class of `User` for modification, adding these two new
methods.
We can see those defined alongside all other direct and inherited class methods:
```ruby
> User.methods
=>
[:find_by_email,
:find_by_last_name,
:yaml_tag,
:allocate,
...
]
```
+37
View File
@@ -0,0 +1,37 @@
# Filter By Type
In Ruby, we have several ways to check if something is a certain type (class or
subclass). A couple common approaches you might see are `#is_a?` and `===`
(case equality operator):
```ruby
> 3.is_a?(Integer)
=> true
> Integer === 3
=> true
> 3 === Integer
=> false
```
Notice it is important to get the ordering of class and value right when using
`===`.
We can use these concepts to filter collections down to just those values of a
certain type. We can also ditch those methods and instead use
[`#grep`](https://ruby-doc.org/3.4.1/Enumerable.html#method-i-grep) to pattern
match on the type directly.
```ruby
> nums = [1, :two, 3.0, 'four', 5, -> { 6 }, 0.7]
=> [1, :two, 3.0, "four", 5, #<Proc:0x0000000123af0338 (irb):5 (lambda)>, 0.7]
> nums.filter { it.is_a?(Numeric) }
=> [1, 3.0, 5, 0.7]
> nums.filter { Integer === it }
=> [1, 5]
> nums.grep(Integer)
=> [1, 5]
> nums.grep(Numeric)
=> [1, 3.0, 5, 0.7]
```
[source](https://bsky.app/profile/lucianghinda.com/post/3mhi5xp3xhk25)
@@ -0,0 +1,25 @@
# Load A Module And Execute A Statement
Here is a nice one-liner pattern for use with the `ruby` executable.
```bash
$ ruby -r file.rb -e 'MyClass.do_something'
```
The `-r` flag loads (requires, really) a Ruby file at the specified path. The
`-e` flag will execute the line of Ruby code that you give it, in that context.
In combination that means I can load some module into the execution environment
and then I can run some code that uses that module.
A more practical example of that is how I demonstrated the behavior of a
`MarkdownHelpers` module in [Create A Module Of Utility
Functions](create-a-module-of-utility-functions.md).
```bash
$ ruby -r ./markdown_helpers.rb -e 'puts MarkdownHelpers.link("Click here", "https://example.com")'
[Click here](https://example.com)
```
The `MarkdownHelpers` module that I've defined in `./markdown_helpers.rb` is
loaded into context and I can now access and execute that module to try out
parts of it. All in a single line in the terminal.
@@ -0,0 +1,38 @@
# Make A Long String Of Text Readable
I have a paragraph of text that interpolates a couple user-specific values
before being included in an API request. Because it is being passed to an API,
it is a single-line string value. However, in the editor it is hard to read like
that because it overflows way past the edge of the viewport.
```ruby
description = "This is the description we need to provide for #{user.name} as part of an API request dealing with compliance and registration for a service. If you need to contact them, their email is #{user.email}."
```
I'd rather make this easier on myself and others to read from the editor while
still being able to submit a single-line string to the API. That can be
accomplished with a heredoc and some combination or `gsub`, `strip`, and
`squish`.
If we are in a strictly Ruby-only context, we can use `gsub` and `strip` to
collapse line breaks and remove surrounding white space.
```ruby
description = <<~MSG.gsub(/\s+/, ' ').strip
This is the description we need to provide for #{user.name} as part
of an API request dealing with compliance and registration for a
service. If you need to contact them, their email is #{user.email}.
MSG
#=> "This is the description we need to provide for #{user.name} as part of an API request dealing with compliance and registration for a service. If you need to contact them, their email is #{user.email}."
```
Or in a Rails context, I can instead just use `squish`:
```ruby
description = <<~MSG.squish
This is the description we need to provide for #{user.name} as part
of an API request dealing with compliance and registration for a
service. If you need to contact them, their email is #{user.email}.
MSG
#=> "This is the description we need to provide for #{user.name} as part of an API request dealing with compliance and registration for a service. If you need to contact them, their email is #{user.email}."
```
@@ -0,0 +1,43 @@
# Specify Default For Data Definition
Here is what a `Data` definition for the concept of a `Permission` might look
like:
```ruby
Permission = Data.define(:id, :name, :description, :enabled)
perm1 = Permission.new(
id: 123,
name: :can_edit,
description: "User is allowed to edit.",
enabled: true
)
```
However, as we're creating various `Permission` entities, we may find that the
vast majority of them are _enabled_ by default and so we'd like to apply `true`
as a default value.
We cannot do this directly in the `Data` definition, but we can open a block to
override the `initialize` method.
```ruby
Permission = Data.define(:id, :name, :description, :enabled) do
def initialize(:id, :name, :description, enabled: true)
super
end
end
perm1 = Permission.new(
id: 123,
name: :can_edit,
description: "User is allowed to edit."
)
perm1.enabled #=> true
```
Now we're able to create a `Permission` without specifying the `enabled`
attribute and it takes on the default of `true`.
[source](https://dev.to/baweaver/new-in-ruby-32-datadefine-2819#comment-254o8)
@@ -0,0 +1,52 @@
# Add Default Task To List All Tasks
One thing I like about [`just`](https://github.com/casey/just) is that if you
run `just` by itself, the default behavior is to list out all the commands it
can run.
[Taskfile](https://github.com/go-task/task) technically does this as well, but
with a warning at the end:
```
task
task: Available tasks for this project:
* notes: Interactive picker for notes tasks
* notes:diff: Show uncommitted changes in notes
* notes:edit: All-in-one edit, commit, and push notes
* notes:log: Show recent commit history for notes
* notes:open: Opens NOTES.md (syncs latest changes first) in default editor
* notes:push: Commit and push changes to notes submodule
* notes:status: Check status of notes submodule
* notes:sync: Sync latest changes from the notes submodule
task: Task "default" does not exist
```
I prefer to tidy this up a little by adding `task --list` as the _default_ in my
`Taskfile.yml`.
```yml
default:
desc: Show available commands
cmds:
- task --list
```
Now when I run `task` with no arguments, I get this minutely nicer version:
```
task
Alias tip: t
task: [default] task --list
task: Available tasks for this project:
* default: Show available commands
* notes: Interactive picker for notes tasks
* notes:diff: Show uncommitted changes in notes
* notes:edit: All-in-one edit, commit, and push notes
* notes:log: Show recent commit history for notes
* notes:open: Opens NOTES.md (syncs latest changes first) in default editor
* notes:push: Commit and push changes to notes submodule
* notes:status: Check status of notes submodule
* notes:sync: Sync latest changes from the notes submodule
```
Notice there is no `task: Task "default" does not exist` warning at the end.
@@ -0,0 +1,46 @@
# List Processes Running Across All Session
I wanted an overview of all the processes running across all the tmux sessions
that I have running on my machine right now. The `list-panes` command (with the
`-a` flag) gives me a listing of all the panes across all session of the current
tmux server.
That output on its own isn't giving me quite the info I'm looking for though.
With the `-f` (_format_) flag, I can use variables available in that context
like `session_name`, `pane_pid`, and `pane_current_command`.
I can assemble the details I want into a command like this:
```bash
tmux list-panes -a -F "#{session_name}:#{window_index}.#{pane_index} #{pane_pid} #{pane_current_command}"
PLP:1.1 62364 zsh
TIL:1.1 62345 nvim
TIL:1.2 65838 task
TIL:2.1 11428 tmux
client-app:1.1 62373 ssh
client-app:1.2 10796 zsh
client-app:1.3 63081 zsh
client-app:2.1 61115 overmind
client-app:3.1 82608 zsh
visualmode-dev:1.1 52237 zsh
```
This gives me the details I want, but I can take it a step further by piping it
to the `column` command to improve the formatting a little:
```bash
tmux list-panes -a -F "#{session_name}:#{window_index}.#{pane_index} #{pane_pid} #{pane_current_command}" \
| column -t
PLP:1.1 62364 zsh
TIL:1.1 62345 nvim
TIL:1.2 65838 task
TIL:2.1 11428 tmux
client-app:1.1 62373 ssh
client-app:1.2 10796 zsh
client-app:1.3 63081 zsh
client-app:2.1 61115 overmind
client-app:3.1 82608 zsh
visualmode-dev:1.1 52237 zsh
```
See `man tmux` and, in particular, the `FORMATS` section for more details.
@@ -0,0 +1,33 @@
# Apply Successive Filters To Lines In Less
Let's say I've opened a large Rails log file with `less`:
```bash
$ less logs/development.log
```
I have an idea of what I'm looking for, but there is way more noise than signal.
I can start to filter out some of the noise. The `&` command starts a filter
prompt. If I start to filter by something like `INSERT INTO`, then a ton of
lines disappear leaving just those matching that pattern.
Scrolling through the current set of lines, I start to have a better idea of
what I'm looking for, but there is still too much noise. I can apply an
additional successive filter on the remaining lines by hitting `&` again and
entering in another pattern -- e.g. `GoodJob`.
Now I only see lines that contain both `INSERT INTO` and `GoodJob` somewhere in
them.
As `less` puts it:
> Multiple & commands may be entered, in which case only lines which match all
> of the patterns will be displayed.
If I want to undo all the filtering, I just need to enter an empty `&` filter
prompt and it will reset things back to displaying all lines.
> If pattern is empty (if you type & immediately followed by ENTER), any
> filtering is turned off, and all lines are displayed.
See `man less` for more details.
+22
View File
@@ -0,0 +1,22 @@
# Browse And Search Help Docs
There are a lot of tools that don't have dedicated `man` pages, but do have
lengthy output when you pass them the `--help` flag.
We can make those details easier to browse and searchable by piping them to
`less`.
```bash
uv run pytest --help | less
```
First, we see the top of the output inside `less` instead of bottom of the
output right above our next terminal prompt.
From `less`, we can use down and up arrows (or `j` and `k`) to navigate through
the details. We can also jump to a specific word or phrase by searching -- type
`/` and then the pattern we're trying to match. `n` and `N` to go to the next or
previous match, respectively.
See `man less` more more details. And if you like these improvements to viewing
tool usage details, you may also be interested in [a better man page viewer](https://www.visualmode.dev/a-better-man-page-viewer).
@@ -0,0 +1,35 @@
# Combine All My TILs Into A Single File
In [Build A Small Text-based Training
Dataset](https://www.visualmode.dev/build-a-small-text-training-dataset), I went
over my need for a sizeable and interesting corpus of text that I could use as a
training dataset I could use to run against [my own naive Byte Pair Encoding
implementation](https://github.com/jbranchaud/build-an-llm-from-scratch/blob/main/chapter-02/bpe_tokenizer.py).
My repo of hand-written TILs is a great candidate, but I need those smashed all
into one file.
Here is a formatted version of the one-liner I ended up with:
```bash
{
cat README.md; \
find */ -name '*.md' -print0 \
| sort -z \
| xargs -0 -I{} sh -c 'echo "<|endoftext|>"; cat "$1"' _ {}; \
} > combined.md
```
This combines all 1700+ of my TILs into a single file separated by the
`<|endoftext|>` delimiter.
The two things I find most interesting about this command are:
1. The use of a null byte (`\0`) separator between the filenames in case there
is anything weird (like spaces) in those filenames. This starts with
`-print0`. The `-z` of `sort` maintains that null byte separator. And then
`xargs` knows to handle it by the `-0` flag.
2. We can coerce `xargs` into running multiple commands by having it spawn a
single shell process that runs each of those commands. To reliably pass the
filename into that shell process, we have `xargs` constitute it as the second
argument (`$1`) by substituting in the filename where `{}` appears.
+78
View File
@@ -0,0 +1,78 @@
# Diff Two Files In Unified Format
The `diff` command is a standalone utility that can be used to get the
difference between two files. It is similar to what you might expect when
running `git diff` which compares two different versions of the same file. The
`diff` command predates `git` and its unified format is what became the standard
that `git` uses for its own diff implementation.
Running `diff` with two files as is gives output like the following:
```bash
diff startup.sh startup2.sh
10,13c10,14
< declare -A SESSIONS=(
< ["TIL"]="$HOME/dev/jbranchaud/til:setup_til"
< ["PLP"]="$HOME/dev/jbranchaud/pool-league-pro:"
< ["client-app"]="$HOME/dev/client/client-app:"
---
> # Sessions will be created in the order listed here
> SESSIONS=(
> "TIL:$HOME/dev/jbranchaud/til:setup_til"
> "PLP:$HOME/dev/jbranchaud/pool-league-pro:"
> "client-app:$HOME/dev/client/client-app:"
73,74c74,75
< for session_name in TIL PLP client-app; do
< IFS=':' read -r directory setup_function <<<"${SESSIONS[$session_name]}"
---
> for session_config in "${SESSIONS[@]}"; do
> IFS=':' read -r session_name directory setup_function <<<"$session_config"
```
That's readable at a glance, but the unified format (with the `-u` flag) can
provide more context:
```bash
diff -u startup.sh startup2.sh
--- startup.sh 2026-01-10 12:46:52
+++ startup2.sh 2026-01-10 12:48:00
@@ -7,10 +7,11 @@
# Session configurations
# Format: "session_name:directory:setup_function"
-declare -A SESSIONS=(
- ["TIL"]="$HOME/dev/jbranchaud/til:setup_til"
- ["PLP"]="$HOME/dev/jbranchaud/pool-league-pro:"
- ["client-app"]="$HOME/dev/client/client-app:"
+# Sessions will be created in the order listed here
+SESSIONS=(
+ "TIL:$HOME/dev/jbranchaud/til:setup_til"
+ "PLP:$HOME/dev/jbranchaud/pool-league-pro:"
+ "client-app:$HOME/dev/client/client-app:"
)
# Setup function for TIL session
@@ -70,8 +71,8 @@
echo ""
# Create sessions in order
- for session_name in TIL PLP client-app; do
- IFS=':' read -r directory setup_function <<<"${SESSIONS[$session_name]}"
+ for session_config in "${SESSIONS[@]}"; do
+ IFS=':' read -r session_name directory setup_function <<<"$session_config"
create_session "$session_name" "$directory" "$setup_function"
done
```
Here we get additional context like surrounding lines and file name details.
While this is useful on its own, it also has the added benefit of making the
output compatible with other tools we may already be using. For instance, I'm
already using [delta](https://github.com/dandavison/delta) as my [git pager](https://github.com/jbranchaud/dotfiles/blob/main/gitconfig#L51) and [git differ](https://github.com/jbranchaud/dotfiles/blob/main/gitconfig#L139).
With the unified format, I can pipe the output directly to `delta` to get a
better view of the diff that is colorized and includes syntax highlighting.
```bash
diff -u startup.sh startup2.sh | delta
```
@@ -0,0 +1,27 @@
# Display Line Numbers While Using Less
Including line numbers while viewing files with `less` can provide useful
context for understanding where you are within the file. This is especially true
if you've used `&` to filter down to lines that match a pattern.
You can start `less` with line numbers with the `-N` flag (or `--LINE-NUMBERS`
if you really want to spell it out).
```bash
$ less -N log/development.log
```
If you've already started up `less` and wish you had included line numbers,
there is no reason to restart it with the flag. Instead, toggle the line numbers
option on within the `less` process. To do this, type `-N`. It will prompt you
with `Constantly display line numbers (press RETURN)`. Hit enter and line
numbers will appear to the left of each line in the file.
Similarly, to toggle line numbers back off within `less`, hit `-n` (lower-case
`n`), accept the prompt, and back off they go.
Both of these (`-N`/`-n`) are options being set (toggled) via the `-` command.
There are many other options like these that can be configured within a `less`
session in the same way.
See `man less` and find the `-` command and the available `OPTIONS`.
@@ -0,0 +1,44 @@
# Format And Display Small Amounts Of Columnar Data
In [_List Processes Running Across All (tmux)
Sessions](tmux/list-processes-running-across-all-sessions.md), I showed an
example of piping some data from `tmux` to the `column -t` command to nicely format
and display the columnar data as a table. By default is uses spaces as the
delimiter.
```bash
tmux list-panes -a -F "#{session_name}:#{window_index}.#{pane_index} #{pane_pid} #{pane_current_command}" \
| column -t
PLP:1.1 62364 zsh
TIL:1.1 62345 nvim
TIL:1.2 65838 task
TIL:2.1 11428 tmux
client-app:1.1 62373 ssh
client-app:1.2 10796 zsh
client-app:1.3 63081 zsh
client-app:2.1 61115 overmind
client-app:3.1 82608 zsh
visualmode-dev:1.1 52237 zsh
```
This can be useful for formatting data from all kinds of commands and tools.
Sometimes the columns of data are separated by something other than spaces. For
instance, here is some git branch information (for my [dotfiles
repo](https://github.com/jbranchaud/dotfiles)) separated by the `|` character.
To format that with `column`, I need to also include the `-s '|'` flag to
override the delimiter.
```bash
git for-each-ref --format='%(refname:short)|%(authordate:short)|%(authorname)' refs/heads/ \
| column -t -s '|'
claude/sync-dotfiles-011CUP87cRV6c51eEi3Chg99 2025-10-22 jbranchaud
jb/add-rhubarb-for-fugitive-github-browse 2025-11-02 jbranchaud
jb/fix-hardcoded-paths 2025-11-02 jbranchaud
jb/set-nvim-to-default-manpager 2025-10-19 jbranchaud
main 2026-01-10 jbranchaud
master 2025-10-30 Dorian Karter
my-dotfiles 2025-11-01 jbranchaud
upstream-master 2026-01-01 Dorian Karter
```
@@ -0,0 +1,47 @@
# Inspect EXIF Data For An Image File
The `exiftool` CLI (which can be downloaded via `brew`) is a useful tool for
inspecting all the EXIF data attached to a media file. A media file like an
image has a bunch of additional details embedded in it like timestamps, image
metadata, and sometimes location information.
Here is all the data attached to a screenshot I found on my desktop:
```bash
exiftool ~/Desktop/CleanShot\ 2025-11-17\ at\ 11.22.18@2x.png
ExifTool Version Number : 13.50
File Name : CleanShot 2025-11-17 at 11.22.18@2x.png
Directory : /Users/lastword/Desktop
File Size : 1194 kB
File Modification Date/Time : 2025:11:17 11:22:21-06:00
File Access Date/Time : 2025:12:15 10:43:55-06:00
File Inode Change Date/Time : 2025:12:05 15:37:48-06:00
File Permissions : -rw-r--r--
File Type : PNG
File Type Extension : png
MIME Type : image/png
Image Width : 2502
Image Height : 1232
Bit Depth : 8
Color Type : RGB with Alpha
Compression : Deflate/Inflate
Filter : Adaptive
Interlace : Noninterlaced
XMP Toolkit : XMP Core 6.0.0
Y Resolution : 144
Resolution Unit : inches
X Resolution : 144
Exif Image Width : 2502
Color Space : sRGB
User Comment : Screenshot
Exif Image Height : 1232
SRGB Rendering : Perceptual
Image Size : 2502x1232
Megapixels : 3.1
```
This works with other kinds of media files. For instance, I ran this against an
MP4 screen recording file which contained even more metadata.
In addition to reading data, `exiftool` can also write it. See `man exiftool`
for more details on what else it can do.
+64
View File
@@ -0,0 +1,64 @@
# Reverse Each Line Of A File
The [`rev` command](https://man7.org/linux/man-pages/man1/rev.1.html) can be
used to reverse each line in a file. Every line is left where it is relative to
other lines, but the contents of each line is reversed.
So a file that contains the following text:
```bash
cat stuff.md
Three
Two
One
go racecar go
```
can be piped to `rev` to get the following output:
```bash
rev stuff.md
eerhT
owT
enO
og racecar og
```
This is an odd utility that doesn't have too much use that I can imagine. After
a brief chat with Claude where I asked for some practical use cases, the one
that stood out the most to me is to reverse a list of filenames, sort them, and
then reverse them again (putting them back in readable order). This can shuffle
filenames with similar endings near each other like source and test files.
Here is a list of files for me [`py-vmt`
project](https://github.com/jbranchaud/py-vmt):
```bash
fd -t f .
README.md
pyproject.toml
src/py_vmt/__init__.py
src/py_vmt/cli.py
src/py_vmt/session.py
src/py_vmt/time_helpers.py
tests/src/py_vmt/test_cli.py
tests/src/py_vmt/test_session.py
```
Now I can pipe the output of that `fd` command through `rev | sort | rev` to get
my files organized in a different way.
```bash
fd -t f . | rev | sort | rev
README.md
pyproject.toml
src/py_vmt/__init__.py
tests/src/py_vmt/test_cli.py
src/py_vmt/cli.py
tests/src/py_vmt/test_session.py
src/py_vmt/session.py
src/py_vmt/time_helpers.py
```
Again the value of doing something like this is a bit tenuous. At the very least
it is fun to know about.
@@ -0,0 +1,36 @@
# Show Tree View Of Processes And Subprocesses
Though you can cobble together a command on a MacOS Unix system to output a
hierarchical tree view of a parent process and its descendent subprocesses, it
is easier to [`brew install pstree`](https://github.com/FredHucht/pstree) and
use that.
Here is what I see when I run it for a _pid_ that corresponds to a `tmux`
session that I have running locally:
```bash
pstree 61690
-+= 61690 lastword tmux new-session -d -s TIL -c /Users/lastword/dev/jbranchaud/til
|--= 63081 lastword /bin/zsh
|-+= 11428 lastword zsh
| \-+= 48511 lastword pstree 61690
| \--- 48512 root ps -axwwo user,pid,ppid,pgid,command
|-+= 62345 lastword zsh
| \--= 06031 lastword claude
|--= 62364 lastword /bin/zsh
|-+= 62373 lastword zsh
| \--= 64407 lastword ssh my-app-staging
|-+= 61115 lastword /bin/zsh
| \-+= 61579 lastword overmind start -f Procfile.dev
| \--- 61586 lastword tmux -C -L overmind-my-app-abc123 new -n web -s my-app -P -F %overmind-process #{pane_id} web #{pane_pid} /var/folders/zc/abc123/T/overmin
|--= 52237 lastword /bin/zsh
|--= 82608 lastword /bin/zsh
\--= 10796 lastword /bin/zsh
```
I was looking for a frozen `claude` process that was part of this session. And I
found it about halfway down that list -- `06031`. Now I can run `kill` on that
process as needed.
For some additional context, I initially found the _pid_ for the `tmux` session
by running `ps aux | grep tmux` and looking through those results.
@@ -0,0 +1,42 @@
# Use Negative Lookbehind Matching With ripgrep
The most straightforward way to use `ripgrep` is to hand it a pattern. It will
take that pattern and move forward through each file trying to find matches.
```bash
$ rg 'TwilioClient\.new'
```
That will find all occurrences of `TwilioClient.new` in available project files.
What if that pattern is too permissive though? That is going to match on
occurrences of `TwilioClient.new` as well as things like
`LoggingTwilioClient.new`. If we want to exclude the latter, there are a few
ways to do that. One of them being the use of [the _negative lookbehind_ regex
feature](https://www.pcre.org/current/doc/html/pcre2syntax.html#SEC23) that is
available with PCRE2 (Perl-Compatible Regular Expressions).
A _negative lookbehind_ is like a standard pattern. We look forward through the
document for the base pattern (like `TwilioClient\.new`). However, once we find
that match, we then look back at the previous characters and if they match our
negative lookbehind pattern, then it is no longer a positive match.
We can use one of the following to forms to achieve this:
```
(?<!...) )
(*nlb:...) ) negative lookbehind
(*negative_lookbehind:...) )
```
For instance, here is what this looks like for our example:
```bash
$ rg -P '(?<!Logging)TwilioClient\.new'
```
Note: we have to use the `-P` flag to tell `ripgrep` that we are using PCRE2
syntax. Otherwise, it assumes a simpler regex syntax that doesn't support
_negative lookbehind_.
See `man rg` for more details.
@@ -0,0 +1,30 @@
# Use The Readline Keybindings Anywhere
There are these features of the "shell" that I've often heard called _emac
keybindings_. These are things like `ctrl-a` (move the cursor to the beginning
of the line) and `ctrl-e` (move the cursor to the end of the line) that I use
every single day. There are several others that are in my heavy rotation,
however, I learned about a couple more reading through [Shell Tricks That
Actually Make Life Easier (And Save Your
Sanity)](https://blog.hofstede.it/shell-tricks-that-actually-make-life-easier-and-save-your-sanity/).
These are [Readline commands](https://www.gnu.org/software/bash/manual/html_node/Bindable-Readline-Commands.html)
(or keybindings) which means they are supported by anything that uses Readline
under the hood. So while you might be using these to great effect in `bash` and
`zsh`, you should look for other places they are available.
A non-exhaustive list includes:
- Ruby's `irb`
- Python's `python`
- Node.js' `node`
- PostgreSQL's `psql`
- Claude Code
And many more similar REPLs and command line tools.
Try these keybindings out in one of your favorites and when you're done hit
`ctrl-c` to exit out of it.
PS. subsets of these keybindings are sometimes supported in unexpected places
like the Chrome URL bar.
@@ -0,0 +1,40 @@
# View Nicely Formatted Markdown From Terminal
The [`glow`](https://github.com/charmbracelet/glow) utility is CLI markdown
renderer written in Go. It is part of the CCU
([charmbraclet](https://github.com/charmbracelet) CLI universe). And yes, I just
made up _CCU_.
`glow` is great because it processes and outputs a markdown file with some
styling tailored to a terminal including:
- colors to emphasize things like headings
- styling of inline code snippets
- syntax highlighting for fenced code blocks
- rendering of markdown tables
- and a lot more that I'm not thinking to mention
In the past I've installed this with `brew`, but I currently manage my `glow`
install with [this mise config](https://github.com/jbranchaud/dotfiles/blob/main/config/mise/config.toml?plain=1#L66).
To view a nicely rendered markdown file, I can run:
```bash
$ glow README.md
```
For long markdown files like [this `README.md`](https://github.com/jbranchaud/til/blob/master/README.md), this
doesn't work too well because it renders until the end and spits you at at the
bottom.
Fortunately, `glow` has a built-in pager that maintains all the styling while
allowing you to navigate and search similar to `less`.
```bash
$ glow -p README.md
```
There is also a TUI version (`-t`), but I find that less intuitive and useful
than the pager.
See `glow --help` for more details.
+44
View File
@@ -0,0 +1,44 @@
# List Available Zle Keybindings
Unlike `bash` which uses `readline`, `zsh` has its own implementation of a line
editing library -- `zle`. A lot of the core bindings between the two are the
same, e.g. `Ctrl-a` and `Ctrl-e` to go the beginning and end of the command line
prompt, respectively.
All available `zle` keybindings can be listed out by running `bindkey` without
any arguments.
The best way to check out an unaltered version of this list is by starting a
fresh `zsh` process with no RCS files loaded in. The `-f` flag does that. Note
though that when `zsh` is starting fresh, it has to decide whether to start in
_Emacs_ mode or _Vi_ mode. If it sees that your default editor is something like
`vi`, `vim` or `nvim`, then it will start you in _Vi_ mode.
Starting in _Vi_ mode can be confusing because none of the standard _Emacs_
keybindings like `Ctrl-a` and `Ctrl-e` are available in that context. So first
ensure you're in _Emacs_ mode by running:
```sh
zsh -f
lastword% bindkey -e
```
Now you can list out all the keybindings:
```sh
lastword% bindkey
"^@" set-mark-command
"^A" beginning-of-line
"^B" backward-char
"^D" delete-char-or-list
"^E" end-of-line
"^F" forward-char
"^G" send-break
"^H" backward-delete-char
"^I" expand-or-complete
"^J" accept-line
"^K" kill-line
...
```
See `man zshzle` for more details on `zle` and `bindkey`.