mirror of
https://github.com/jbranchaud/til
synced 2026-07-05 17:00:17 +00:00
Compare commits
88 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| facc606014 | |||
| be103b52dd | |||
| 3402428aad | |||
| 0c00e47141 | |||
| 1c90fdd823 | |||
| bf3991ce04 | |||
| 3db5af78c1 | |||
| a8c35e2458 | |||
| c0ad3cee4d | |||
| 9bd1bb413a | |||
| ab8331000f | |||
| cd54360925 | |||
| 75421685ea | |||
| 0cb5890fc0 | |||
| 7de0e70d78 | |||
| 36934aa56f | |||
| 2cd465bb08 | |||
| 6ad376885b | |||
| 0c4702be97 | |||
| b873f86f5b | |||
| 1120bb2018 | |||
| 906253b7dc | |||
| b4920c0397 | |||
| 119cc15c9a | |||
| 1a4589f8f7 | |||
| 5f35404433 | |||
| 1766e45134 | |||
| c875652725 | |||
| 8af252f232 | |||
| eb0a7e1b3d | |||
| c1cd40311f | |||
| c744117eff | |||
| 329ce1aa3e | |||
| 16082177aa | |||
| 2276a57445 | |||
| ceaab3da4f | |||
| c7711ca337 | |||
| 11279ac362 | |||
| 01f9d89e8e | |||
| e1c3f23975 | |||
| 16ad6bd64d | |||
| f2a6fddba8 | |||
| e2603f1445 | |||
| c1f046d196 | |||
| 55a6691681 | |||
| 3632cfbe1b | |||
| e82bc873b9 | |||
| 43ade88fab | |||
| 8f99085e4b | |||
| f20428b06a | |||
| e41802653d | |||
| 2cc52bf8bc | |||
| df418b5718 | |||
| d084e0ffe0 | |||
| 72b466a8b3 | |||
| be18f387ed | |||
| efb83050ab | |||
| ec12f7ea80 | |||
| f186d5977d | |||
| f967520fa3 | |||
| 1517e1fb7a | |||
| f56d93b49b | |||
| dd6350aa41 | |||
| bdd3adf577 | |||
| f48adc0f05 | |||
| 9773f10b84 | |||
| bd58be8fda | |||
| 35f1f0b807 | |||
| 81afd44913 | |||
| 6fdadfa1fb | |||
| aaf7da413c | |||
| 6a2a0a8ac1 | |||
| 712fc66aae | |||
| e95477607e | |||
| 087766a792 | |||
| 4801e730f9 | |||
| bd021f7eab | |||
| 8d8cfd56ce | |||
| f4faa06258 | |||
| 8ccbd82320 | |||
| 5ea4165893 | |||
| 73476a8d16 | |||
| c5ce81f918 | |||
| 32be787998 | |||
| 1d835d3553 | |||
| 0d4959046d | |||
| b1198d2488 | |||
| 6c3805e7cd |
@@ -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
|
||||||
|
|
||||||
© 2015-2025 Josh Branchaud
|
© 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
@@ -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}}'
|
||||||
|
|||||||
@@ -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`.
|
||||||
@@ -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.
|
||||||
@@ -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)
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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:**"`.
|
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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]
|
||||||
|
```
|
||||||
@@ -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.
|
||||||
@@ -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)
|
||||||
@@ -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).
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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,
|
||||||
|
...
|
||||||
|
]
|
||||||
|
```
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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`.
|
||||||
Reference in New Issue
Block a user