Compare commits

10 Commits
10 changed files with 483 additions and 1 deletions
+10 -1
View File
@@ -10,7 +10,7 @@ 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).
_1868 TILs and counting..._ _1877 TILs and counting..._
See some of the other learning resources I work on: See some of the other learning resources I work on:
@@ -172,6 +172,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [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) - [Resume Specific Session](claude-code/resume-specific-session.md)
- [Set Permission Mode When Starting Session](claude-code/set-permission-mode-when-starting-session.md)
- [Stash The Current Prompt To Send Another First](claude-code/stash-the-current-prompt-to-send-another-first.md) - [Stash The Current Prompt To Send Another First](claude-code/stash-the-current-prompt-to-send-another-first.md)
### Clojure ### Clojure
@@ -1105,6 +1106,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [Escape Curly Braces Within Formatted String](python/escape-curly-braces-within-formatted-string.md) - [Escape Curly Braces Within Formatted String](python/escape-curly-braces-within-formatted-string.md)
- [Experiment With SQLite Queries In Memory](python/experiment-with-sqlite-queries-in-memory.md) - [Experiment With SQLite Queries In Memory](python/experiment-with-sqlite-queries-in-memory.md)
- [Force Remaining Arguments To Be Named](python/force-remaining-arguments-to-be-named.md) - [Force Remaining Arguments To Be Named](python/force-remaining-arguments-to-be-named.md)
- [Generate Sample PDFs With ReportLab](python/generate-sample-pdfs-with-reportlab.md)
- [Get Absolute Seconds From `timedelta` Object](python/get-absolute-seconds-from-timedelta-object.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) - [Get Quotient And Remainder In One Operation](python/get-quotient-and-remainder-in-one-operation.md)
- [Globally Install CLI Tool With UV](python/globally-install-cli-tool-with-uv.md) - [Globally Install CLI Tool With UV](python/globally-install-cli-tool-with-uv.md)
@@ -1124,12 +1126,14 @@ If you've learned something here, support my efforts writing daily TILs by
- [Parse Relative Time To datetime Object](python/parse-relative-time-to-datetime-object.md) - [Parse Relative Time To datetime Object](python/parse-relative-time-to-datetime-object.md)
- [Publish A Package To A Test Env As A Dry Run](python/publish-a-package-to-a-test-env-as-a-dry-run.md) - [Publish A Package To A Test Env As A Dry Run](python/publish-a-package-to-a-test-env-as-a-dry-run.md)
- [Reclassify Certain Packages As Dev Dependencies](python/reclassify-certain-packages-as-dev-dependencies.md) - [Reclassify Certain Packages As Dev Dependencies](python/reclassify-certain-packages-as-dev-dependencies.md)
- [Register SQLite Adapter To Serialize Datetimes](python/register-sqlite-adapter-to-serialize-datetimes.md)
- [Resurface Exceptions Swallowed By Click Under Test](python/resurface-exceptions-swallowed-by-click-under-test.md) - [Resurface Exceptions Swallowed By Click Under Test](python/resurface-exceptions-swallowed-by-click-under-test.md)
- [Select Implementation With Class Registry](python/select-implementation-with-class-registry.md) - [Select Implementation With Class Registry](python/select-implementation-with-class-registry.md)
- [Set Up Pyright Type Checking In GitHub](python/set-up-pyright-type-checking-in-github.md) - [Set Up Pyright Type Checking In GitHub](python/set-up-pyright-type-checking-in-github.md)
- [Skip Specific Pytest Test Cases](python/skip-specific-pytest-test-cases.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 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) - [Sort Normalized Version Of Data](python/sort-normalized-version-of-data.md)
- [Specify Parameter Name For Click Option](python/specify-parameter-name-for-click-option.md)
- [Start Jupyter Notebook With Extra Packages](python/start-jupyter-notebook-with-extra-packages.md) - [Start Jupyter Notebook With Extra Packages](python/start-jupyter-notebook-with-extra-packages.md)
- [Start The Debugger When A Test Errors](python/start-the-debugger-when-a-test-errors.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)
@@ -1648,9 +1652,11 @@ If you've learned something here, support my efforts writing daily TILs by
### SQLite ### SQLite
- [Add Check Constraint To Existing Column](sqlite/add-check-constraint-to-existing-column.md)
- [Display Results In Readable Column Format](sqlite/display-results-in-readable-column-format.md) - [Display Results In Readable Column Format](sqlite/display-results-in-readable-column-format.md)
- [Explore The Database Schema](sqlite/explore-the-database-schema.md) - [Explore The Database Schema](sqlite/explore-the-database-schema.md)
- [Manage Lightweight Schema Migrations With `user_version`](sqlite/manage-lightweight-schema-migrations-with-user-version.md) - [Manage Lightweight Schema Migrations With `user_version`](sqlite/manage-lightweight-schema-migrations-with-user-version.md)
- [Output Query Result In Nicely Formatted Table](sqlite/output-query-result-in-nicely-formatted-table.md)
### Streaming ### Streaming
@@ -1680,6 +1686,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [Change Base Directory Of Existing Session](tmux/change-base-directory-of-existing-session.md) - [Change Base Directory Of Existing Session](tmux/change-base-directory-of-existing-session.md)
- [Change Base Directory Without Detaching](tmux/change-base-directory-without-detaching.md) - [Change Base Directory Without Detaching](tmux/change-base-directory-without-detaching.md)
- [Change The Default Prefix Key](tmux/change-the-default-prefix-key.md) - [Change The Default Prefix Key](tmux/change-the-default-prefix-key.md)
- [Connect To Individual Overmind Processes Via tmux](tmux/connect-to-individual-overmind-processes-via-tmux.md)
- [Create A Named tmux Session](tmux/create-a-named-tmux-session.md) - [Create A Named tmux Session](tmux/create-a-named-tmux-session.md)
- [Create A New Session In A New Server](tmux/create-a-new-session-in-a-new-server.md) - [Create A New Session In A New Server](tmux/create-a-new-session-in-a-new-server.md)
- [Cycle Through Layouts](tmux/cycle-through-layouts.md) - [Cycle Through Layouts](tmux/cycle-through-layouts.md)
@@ -1752,6 +1759,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [Check SSH Key Fingerprints Of Known Hosts](unix/check-ssh-key-fingerprints-of-known-hosts.md) - [Check SSH Key Fingerprints Of Known Hosts](unix/check-ssh-key-fingerprints-of-known-hosts.md)
- [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)
- [Check What Is Inside A Zip File](unix/check-what-is-inside-a-zip-file.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) - [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)
@@ -2143,6 +2151,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [Open Slack's Keyboard Shortcuts Reference Panel](workflow/open-slacks-keyboard-shortcuts-reference-panel.md) - [Open Slack's Keyboard Shortcuts Reference Panel](workflow/open-slacks-keyboard-shortcuts-reference-panel.md)
- [Pop Videos Out As Picture-in-Picture](workflow/pop-videos-out-as-picture-in-picture.md) - [Pop Videos Out As Picture-in-Picture](workflow/pop-videos-out-as-picture-in-picture.md)
- [Prune The Excess From node_modules](workflow/prune-the-excess-from-node-modules.md) - [Prune The Excess From node_modules](workflow/prune-the-excess-from-node-modules.md)
- [Remove Pages From A PDF](workflow/remove-pages-from-a-pdf.md)
- [Rotate An Image To Be Oriented Upright](workflow/rotate-an-image-to-be-oriented-upright.md) - [Rotate An Image To Be Oriented Upright](workflow/rotate-an-image-to-be-oriented-upright.md)
- [See Overlaps For A Set Of Time Zones](workflow/see-overlaps-for-a-set-of-time-zones.md) - [See Overlaps For A Set Of Time Zones](workflow/see-overlaps-for-a-set-of-time-zones.md)
- [Send A Message To A Discord Channel](workflow/send-a-message-to-a-discord-channel.md) - [Send A Message To A Discord Channel](workflow/send-a-message-to-a-discord-channel.md)
@@ -0,0 +1,29 @@
# Set Permission Mode When Starting Session
The way I typically use Claude Code day-to-day is with a couple long-running
sessions for one to two clones of the project. I start a session with `claude`
and then hit `shift+tab` until I've toggled it to _auto_ mode. I do tightly
scoped features and `/clear` the context in between each.
I get used to being in _auto_ mode, so whenever I start a new `claude` session I
forget to first toggle from _manual_ to _auto_ mode.
This is where the
[`--permission-mode`](https://code.claude.com/docs/en/permission-modes) flag can
help. I can start a session directly in _auto_ mode like so:
```bash
claude --permission-mode auto
```
Or if I know I want to generate a plan first, I can start it in _plan_ mode.
```bash
claude --permission-mode plan
```
There is also the `--dangerously-skip-permissions` flag which is equivalent to
`--permission-mode bypassPermissions`. I tend to stay away from those unless I'm
working from a sandboxed dev container.
See `claude --help` for more details.
@@ -0,0 +1,44 @@
# Generate Sample PDFs With ReportLab
The `reportlab` package has a `pdfgen` module that provides a `Canvas` API for
programmatically constructing a PDF.
I recently needed the most basic use of this to quickly construct a sample PDF
that I could use as I experimented with [different `qpdf`
commands](/workflow/remove-pages-from-a-pdf.md). I wanted to generate a 10-page
PDF where each page included a large font number in the center. This way I could
clearly see what was the first, second, third, and so on pages. Then I could
quickly verify that the `qpdf` manipulations I was doing were working as
expected.
I was able to do this without any setup or install. Using a `uv` one-liner (does
a 10-line heredoc count as a one-liner?), I generated a PDF with some basic
canvas rendering on each page.
```python
uv run --with reportlab python - <<'EOF'
from reportlab.pdfgen import canvas
N, W, H = 10, 612, 792
c = canvas.Canvas("test.pdf", pagesize=(W, H))
for i in range(1, N + 1):
c.setFont("Helvetica-Bold", 220)
c.drawCentredString(W / 2, H / 2 - 80, str(i))
c.setFont("Helvetica", 28)
c.drawCentredString(W / 2, 100, f"page {i} of {N}")
c.showPage()
c.save()
EOF
```
The `--with` makes the `reportlab` package available to the command being run.
The command being run is `python` which evaluates that block of heredoc text.
Within the heredoc, I import `canvas` from the `reportlab` package that has been
made available. I then create an in-memory PDF of a standard size. Then for-loop
10 times to build each page of the PDF with a big number in the middle and a
smaller `page x of N` lower down on the page. Finally, I save the PDF which
writes it to the directory I executed this whole command from.
I had Claude help with the `canvas` API, however there are some decent
[`reportlab.pdfgen` docs
here](https://docs.reportlab.com/reportlab/userguide/ch2_graphics/).
@@ -0,0 +1,93 @@
# Register SQLite Adapter To Serialize Datetimes
SQLite doesn't have a datetime or timestamp data type, so that information has
to be stored as `text` or a unix epoch `int`. That means when using Python's
`sqlite3` module to perform writes I need to tell it what shape to write a
datetime value.
I could manually convert `datetime` values everywhere they are involved in
_write_ operations.
```python
# Prepare `sessions` insert payload
session_data = {
"active": 1 if active else 0,
"project_id": project_id,
"start_time": datetime.isoformat(session.start_time),
"end_time": None,
}
if session.end_time:
session_data["end_time"] = datetime.isoformat(session.end_time)
# Insert the new active session
cursor = self.conn.execute(
"""
insert into sessions (active, project_id, start_time, end_time)
values (:active, :project_id, :start_time, :end_time)
returning id;
""",
session_data,
)
```
I've used `datetime.isoformat` above which formats `datetime` objects like so:
```python
>>> datetime.now().isoformat()
'2026-08-28T11:52:04.709907'
```
I'd like to make two improvements.
1. I want these `datetime` values to be formatted instead like `2026-08-28T18:15:27.213Z`.
2. I want `datetime` values to be serialized automatically in the specific shape
without having to manually convert them everywhere.
I can achieve both of these things by [registering an adapter with `sqlite3`](https://docs.python.org/3/library/sqlite3.html#how-to-register-adapter-callables)
that handles the serialize of `datetime` objects.
First, I define a function that can perform the `datetime` to `str` conversion.
I decided to put this in `db.py` where I have some other database-specific
functions.
```python
from datetime import datetime, timezone
def to_db(dt: datetime) -> str:
if dt.tzinfo is None or dt.utcoffset() is None:
raise ValueError(f"Unable to store naive datetime: {dt!r}")
dt = dt.astimezone(timezone.utc)
return f"{dt:%Y-%m-%dT%H:%M:%S}.{dt.microsecond // 1000:03d}Z"
```
Then I register the adapter before creating the connection that gets used for
database interactions.
```python
import sqlite3
from datetime import datetime
from pathlib import Path
from sqlite3 import Connection
def initialize_conn(db_file: Path) -> Connection:
# register adapters
sqlite3.register_adapter(datetime, to_db)
conn: Connection = sqlite3.connect(db_file)
conn.row_factory = sqlite3.Row
return conn
```
Then I can run write operations with `datetime` objects knowing they will be
correctly serialized.
```python
with self.conn:
query = "update sessions set active = :active, end_time = :end_time where active = 1;"
self.conn.execute(
query,
{"active": 0, "end_time": session.end_time},
)
```
@@ -0,0 +1,55 @@
# Specify Parameter Name For Click Option
[Click](https://click.palletsprojects.com/en/stable/)'s option decorator
provides a versatile way to define flags for a command. It has good defaults
that minimize the aspects of a flag that I need to be explicit about.
For example, a boolean `--init` flag for the `config` command could be specified
like so:
```python
@cli.command()
@click.option(
"--init",
help="Initialize a config file with minimal defaults",
is_flag=True,
)
@pass_cli
def config(cli_ctx: CliContext, init: bool):
# ...
```
Notice, in particular, that the flag string (`--init`) that I pass as the first
argument to `@click.option` has to correspond to the name of the parameter
`init`. Click passes all the defined options as keyword arguments when invoking
`config`. If the `init` parameter was changed to `initial`, there would be a
runtime error like this: `TypeError: config() got an unexpected keyword argument 'init'`.
Like I said though, Click is flexible when I need it to be. I can leave the flag
name as it is, but specify a different name to be used for the function
parameter. I found this useful when I realized that as I added support for a
`--json` flag I was inadvertently superseding the `json` import.
The second positional argument to `@click.option` can be included to rename that
parameter:
```python
import json
@cli.command()
@click.option(
"--json",
"use_json",
help="Output all info details in JSON format",
is_flag=True,
)
@pass_cli
def info(cli_ctx: CliContext, use_json: bool):
# ...
if use_json:
click.echo(json.dumps(info_details, indent=2))
```
Both of these code blocks are excerpts from my [`py-vmt` time tracker project](https://github.com/jbranchaud/py-vmt).
@@ -0,0 +1,74 @@
# Add Check Constraint To Existing Column
To not bury the lede, SQLite does not support adding a check constraint to an
existing column. Instead, if I want to add one after the fact, I can reconstruct
the table with the check constraint specified up front and then migrate the
data.
Here is a `sessions` table that includes `start_time` and `end_time` columns
that use the `text` data type to represent points in time.
```sql
sqlite> .schema sessions
CREATE TABLE sessions (
id integer primary key,
active integer not null check (active in (0, 1)),
project_id integer not null references projects(id) on delete cascade,
start_time text not null,
end_time text,
created_at text not null default (datetime('now')),
updated_at text not null default (datetime('now'))
);
CREATE UNIQUE INDEX idx_sessions_single_active
on sessions(active)
where active = 1;
```
I want `start_time` and `end_time` to both enforce the shape of the timestamp
strings with `check` constraints. To do that, I need to start a transaction,
create a new version of the table with the check constraints, migrate the data,
rename `sessions` to `sessions_old`, rename `sessions_new` to `sessions` (that's
the in-place swap), and then commit the transaction. `sessions_old` can be
dropped later once I feel good about the migration.
```sql
begin transaction;
create table sessions_new (
id integer primary key,
active integer not null check (active in (0, 1)),
project_id integer not null references projects(id) on delete cascade,
start_time text not null,
end_time text,
created_at text not null default (datetime('now')),
updated_at text not null default (datetime('now')),
check(start_time is strftime('%Y-%m-%dT%H:%M:%fZ', start_time)),
check(end_time is strftime('%Y-%m-%dT%H:%M:%fZ', end_time))
);
insert into sessions_new (
id, active, project_id, start_time, end_time, created_at, updated_at
)
select id, active, project_id, start_time, end_time, created_at, updated_at
from sessions;
alter table sessions rename to sessions_old;
alter table sessions_new rename to sessions;
drop index if exists idx_sessions_single_active; -- on sessions_old
create unique index idx_sessions_single_active
on sessions(active)
where active = 1;
commit;
```
Notice that after the table renames I also drop index (now pointing to
`sessions_old`) and recreate it for the _new_ `sessions` table.
This approach worked well for my situation, but may not be a one-size-fits-all
solution. Depending on how the database is deployed, the size of the tables, and
usage, this approach may not scale. Always do a dry-run of database migrations
like this.
@@ -0,0 +1,50 @@
# Output Query Result In Nicely Formatted Table
When I start a fresh SQLite connection and run a query, all the results are
squished together in a way that is poorly formatted, hard to read, and missing
column headers.
```sql
sqlite> select sessions.id, start_time, end_time, projects.name from sessions join projects on projects.id = sessions.project_id limit 3;
1|2026-07-26T21:15:50.062936+00:00|2026-07-26T21:53:13.990Z|taco
2|2026-07-26T21:53:40.019946+00:00|2026-07-26T22:14:09.168Z|TIL
3|2026-08-02T17:00:16.119169+00:00|2026-08-02T17:30:16.119Z|py-vmt
```
I can drastically improve the look of this by turning _headers_ on and switching
to _box_ mode.
```sql
sqlite> .headers on
sqlite> .mode box
sqlite> select sessions.id, start_time, end_time, projects.name from sessions join projects on projects.id = sessions.project_id limit 3;
id start_time end_time name
1 2026-07-26T21:15:50.062936+00:00 2026-07-26T21:53:13.990Z taco
2 2026-07-26T21:53:40.019946+00:00 2026-07-26T22:14:09.168Z TIL
3 2026-08-02T17:00:16.119169+00:00 2026-08-02T17:30:16.119Z py-vmt
```
I personally find that much easier on the eyes. It is also a nicer format to
copy and paste into a post like this or a formatted code block that I'm sharing
with a colleague.
I can also do this directly from the CLI with a one-liner using `-header` and
`-box` like so:
```bash
sqlite3 /Users/lastword/.local/share/vmt/sessions.db -header -box "select sessions.id, start_time, end_time, projects.name from sessions join projects on projects.id = sessions.project_id limit 3"
┌────┬──────────────────────────────────┬──────────────────────────┬────────┐
│ id │ start_time │ end_time │ name │
├────┼──────────────────────────────────┼──────────────────────────┼────────┤
1 │ 2026-07-26T21:15:50.062936+00:00 │ 2026-07-26T21:53:13.990Z │ taco │
2 │ 2026-07-26T21:53:40.019946+00:00 │ 2026-07-26T22:14:09.168Z │ TIL │
3 │ 2026-08-02T17:00:16.119169+00:00 │ 2026-08-02T17:30:16.119Z │ py-vmt │
└────┴──────────────────────────────────┴──────────────────────────┴────────┘
```
Run the `.help` dot-command from a SQLite prompt for a full listing of these
commands. See also `sqlite3 --help` from the CLI for usage details about all
flags.
@@ -0,0 +1,39 @@
# Connect To Individual Overmind Processes Via tmux
A common `Procfile.dev` in a Rails project might look something like this.
```
web: bin/rails server -p $PORT
vite: bin/vite dev
worker: bundle exec good_job start
```
Instead of starting up each process that needs to be running for development to
work, I can instead run a tool that reads the procfile and sets it all up for me
-- like `overmind`.
```bash
overmind start -f Procfile.dev
```
What's cool about `overmind` is that it starts its own `tmux` session and then
runs each of these processes in its own window.
I can connect to any one of them by name with `overmind connect <name>`. Or I
can connect to the session defaulting to the first window with just `overmind
connect`.
If I need to see what is going on with my background jobs, I'll run:
```bash
overmind connect worker
```
This behaves like any other tmux session, so I can use my prefix key (`ctrl-z`
in my case) to access tmux-specific keybindings. Most notably, once I'm done
looking, I'll want to hit `ctrl-z d` to detach from the session.
I'm already using tmux as my daily driver which means its easy for me to end up
in a nested tmux session if I connect while already in my development session.
To help with that, I set up [a forwarding
prefix](set-up-forwarding-prefix-for-nested-session.md).
+31
View File
@@ -0,0 +1,31 @@
# Check What Is Inside A Zip File
Before unzipping a file, it can be useful to check the contents. You can learn a
few things from doing this. First off, are all the files nested under some extra
top-level directory? How many files are in there? How big are certain files? Etc.
The `-l` flag to `unzip` can help answer these questions.
```bash
unzip -l zipped-files.zip
Archive: zipped-files.zip
Length Date Time Name
--------- ---------- ----- ----
0 08-22-2026 08:54 files-to-zip/
14 08-22-2026 08:54 files-to-zip/data.json
38 08-22-2026 08:53 files-to-zip/notes.md
14 08-22-2026 08:53 files-to-zip/hello.txt
--------- -------
66 4 files
```
Notice that the first line is for `files-to-zip/` which tells us that everything
is nested under the top-level directory that was probably the target of the
`zip` command. Everything is organized into a table with labeled columns which
allows us to answer our questions and get a good idea of what is in there.
In my experience, zip files tend to have dozens to hundreds of files, so it is
even better to pipe the above to a pager like `less`. That way you can search
and navigate through everything that's in there.
See `man unzip` for more details.
+58
View File
@@ -0,0 +1,58 @@
# Remove Pages From A PDF
I have a two page PDF. I really only need to second page though. I want to send
that second page to someone and including the first page with it will only cause
confusion. I couldn't figure out a sane way to do this on MacOS with either
Preview or Notes (where the PDF originated).
So, I looked into a tool I'd heard of recently for manipulating PDFs called
[`qpdf`](https://github.com/qpdf/qpdf).
> qpdf is a command-line tool and C++ library that performs content-preserving
> transformations on PDF files.
I installed it with `homebrew` and then looked up how "removing" pages works.
Conceptually, `qpdf` is used to transform a PDF. That means there are a couple
ways I can think about approaching this.
If I want to preserve every page except for the first page, then I produce a PDF
that starts at page `2` and goes to the end (`z`) by specifying a range of
`2-z`.
```bash
qpdf in.pdf --pages . 2-z -- out.pdf
```
If I truly only want the 2nd page from the PDF and nothing else, then I can
produce a PDF made up of _only_ page `2`.
```bash
qpdf in.pdf --pages . 2 -- out.pdf
```
Yet another way to think about this is to produce a PDF that includes all pages,
but then explicitly mark page `1` for exclusion after the range with `x1`.
```bash
qpdf in.pdf --pages . 1-z,x1 -- out.pdf
```
And one last way, assuming that I am dealing specifically with a two-page PDF,
is to use the `rN` syntax where `N` is the number of pages from the back of the
list. `r1` is the last page, `r2` is the second to last page, and so forth. In a
two-page PDF, `r2` would then refer to the _first_page.
```bash
qpdf in.pdf --pages . 1-z,xr2 -- out.pdf
```
I know I said that was the last, but I'll also throw in that, again assuming a
two-page PDF, I could produce a PDF made up of only `:even` numbered pages
relative to the defined range.
```bash
qpdf in.pdf --pages . 1-z:even -- out.pdf
```
I'll stick with the first approach, but it was fun to experiment with all the
other ways this can be done with `qpdf`'s flexible syntax.