| command | move |
|---|---|
| summary | Move a Markdown file and rewrite every reference to it — incoming links and ref-def destinations, the moved file's own outbound relative links, and `[[stem]]` wikilinks when the basename changes — staging the rename with `git mv` when the file is tracked. |
Unreleased.
movelanded after v0.55.1, so the npm, PyPI, and GitHub release binaries at that version answerunknown command "move". It ships with the next release; until then, build frommainwithgo install github.com/jeduden/mdsmith/cmd/mdsmith@main. Delete this note when a release includes the command.
Relocate a Markdown file and rewrite every reference in one
step, so no link breaks in either direction. move and
rename are two verbs over one refactor engine:
move changes a file's path, rename changes a heading slug
or a link-reference label inside a file.
mdsmith move [flags] <src> <dst>
<src> and <dst> are workspace-relative. Absolute paths and
parent-traversal entries (../foo.md) are rejected with exit
code 2.
- Incoming links. Every inline
[text](src)orin the workspace is repointed todst. A?queryor#anchorafter the path is kept. The path token is recomputed relative to each referencing file's own directory, preserving its spelling — an explicit./xkeeps the prefix. - Ref-def destinations. A
[label]: srcdefinition is repointed the same way, including one with a?query. - Outbound links, images, and ref-defs inside the moved
file. Each inline
[x](path),, and[label]: pathinsrcis recomputed so it still resolves fromdst's directory. Movingdocs/a.mdtoguide/a.mdfixes its own[x](./b.md)as well as the links pointing at it. A destination that still resolves, such assub/../b.mdafter a move within one directory, keeps its spelling. A link to a directory, such assub/, keeps its trailing/. A moved file that mdsmith does not lint as Markdown, such as an image, keeps its bytes. One with another extension that thefiles:patterns match, such asx.mdx, is recomputed too. - Wikilinks.
[[old-stem]]becomes[[new-stem]]only when the basename stem changes. A move that keeps the basename (docs/api.md→ref/api.md) leaves wikilinks alone, because a stem still resolves to the file at its new path — an asymmetry with path links that--dry-runmakes visible.
Each destination is found in the parsed document, so each one is rewritten exactly once. These forms are all handled:
- a link with empty text, such as
[](a.md); - a label that spans rows, or a destination on the row after
its
((or after a ref-def's:), in a block quote too; - an angle-bracketed destination, such as
<my file.md>; - a titled destination, such as
[t](a.md "title").
Link-shaped text in a code span, a code block, or an HTML comment is not a destination, so it stays as written, even inside a link's own label.
A percent-escaped destination such as my%20file.md is decoded
before it is compared. The new path is escaped the way the old
one was. my%20file.md stays escaped, and <my file.md> keeps
its literal space.
A character that would break the link is always escaped: a space
in a bare destination, and %, ?, #, <, >, &, \, or
". So a move to what?.md writes what%3F.md. A bare ? would
start a query string and name the file what, and a literal
& would be read as &. A bare destination also escapes its parens
when one has no partner. A move to a).md writes a%29.md, while
a(1).md stays as written. A new path whose first segment holds a
: gets a ./ prefix, so a move to a:b.md writes ./a:b.md. A
bare a:b.md would read as a URL with the scheme a:.
A literal ? is read as the start of a query unless the whole
path names the moved file, of any type, or a Markdown file in the
workspace, and the part before the ? does not. Then
[x](what?.md) is matched as the file what?.md, and the rewrite
writes it as what%3F.md.
Absolute URLs, mailto:, and root-anchored /x paths do not
resolve to a workspace file, so a move never touches them.
A move does not rewrite these references yet. After a cross-directory move, check them by hand.
- Directive paths. A
file:path in<?include?>or aninputs:path in<?build?>, in the moved file or in a file that points at it, and a<?catalog?>glob in the moved file. - Raw HTML links.
<a href="a.md">and<img src="a.png">are not Markdown destinations. - Links above the workspace root. A relative link in the
moved file that climbs out of the workspace, such as
[p](../README.md)ina.md, is never recomputed. After a move toguide/a.md, it names the workspace's ownREADME.md. - Backslash escapes and entities. A path spelled with one,
such as
a\_b.mdora&b.md, is left as written, in the moved file and in the files that point at it. A renderer reads it asa_b.mdora&b.md, which the move does not decode. Write the name out, or percent-escape it as ina%26b.md. A\just before the#or?that ends the path, as ina.md\#x, only escapes that byte, so such a link is repointed and keeps its\. - Ambiguous wikilinks. When another file shares the old or
the new basename stem, no
[[stem]]is rewritten, because the rewrite could point it at the wrong file. - Footnote text that is a lone link. mdsmith reads
[^1]: [z](a.md)as a footnote definition and leaves its text as written, so the link inside it is not repointed. Longer footnote text, such as[^1]: See [z](a.md)., is repointed. - Embeds of a renamed non-Markdown file. Moving an image
repoints the links and images that name it. A wikilink embed
such as
![[diagram.png]]is left alone, so it goes stale when the move changes the file name.
When <src> is tracked in the current Git work tree, move
runs git mv so the rename is staged in the index. Otherwise
it falls back to a plain filesystem rename. The destination's
parent directory is created first. A failed git mv leaves
the source in place, and move restores every file it had
rewritten (see Safety).
The file moves last, after every text edit is written, so the relocated file carries its rewritten body.
move is all-or-nothing. It rewrites every file and moves
<src>, or it exits 2 and undoes what it wrote. It works in
two phases.
- Plan.
movereads every file it will rewrite and computes the new bytes in memory. An existing<dst>, an unreadable file, or an edit that does not apply exits 2 here, before anything is written.--dry-runruns this phase too, so it reports the same errors. Then it prints the edits and the planned move, and stops. - Write.
movewrites the new bytes of each file to a temp file in the same directory, with the permission bits of the original. If this fails, for example on a full disk,movedeletes the temp files and no file changes. Next, each temp file is renamed over its original. Last,<src>is moved. If a rename or the move fails,movewrites the original bytes back to every file it had replaced. Any directory it created for<dst>stays behind, empty. A rewritten file that was a symlink becomes a regular file, and a rollback puts the old bytes in that file, not the link.
An edit does not apply when another edit on its line claims some of the same bytes, or when it falls outside its line. It also fails when it ends on another line than it starts, or when it starts or ends between the two UTF-16 units of a character such as 😀. The error names the file and the line.
A plan failure prints only its cause. Nothing was written. After a failed write, stderr names the cause. Its last line gives the state of the workspace:
no file was changed: the failure came before any file was replaced.restored N file(s) to their original content: the rollback put back every replaced file.N file(s) keep the rewritten content: <files>: the rollback could not restore these files. Arestoring <file>line gives the reason for each one. Restore them by hand, or withgit restorewhen they are tracked.
This guarantee covers the failures move can see. A crash or
a power loss in the write phase can leave some files rewritten
and others not. Each file is still whole: it holds the old
bytes or the new bytes, never a mix of both. Temp files named
<file>.<digits>.tmp may be left next to the files; delete
them.
| Flag | Default | Description |
|---|---|---|
--dry-run |
false | Print the edits and planned move; write nothing |
-c, --config |
auto | Override config path |
-f, --format |
text |
Output format: text or json |
--no-gitignore |
false | Disable .gitignore filtering during walk |
--follow-symlinks |
config | Follow symlinks; tri-state — see below |
--max-input-size |
2MB |
Max file size (e.g. 2MB, 0=none) |
--follow-symlinks and file discovery (the files: and
ignore: patterns in .mdsmith.yml) match
mdsmith check.
The rewritten files, one per line, then the move.
text (default):
docs/index.md: 2 edit(s)
guide/api.md: 1 edit(s)
moved docs/api.md -> guide/api.md
A --dry-run writes would move in place of moved.
json:
{
"files": [
{ "file": "docs/index.md", "edits": 2 }
],
"move": { "from": "docs/api.md", "to": "guide/api.md" }
}Move a file and fix every reference:
mdsmith move docs/api.md reference/api.mdPreview the edits without touching disk:
mdsmith move guide.md reference/guide.md --dry-run| Code | Meaning |
|---|---|
| 0 | Moved |
| 1 | Source not found |
| 2 | Existing destination, traversal, or a failed edit, write, or git mv |
mdsmith rename— retitle a heading or a link-reference label inside a file.mdsmith deps— the dependency edges a move walks to find incoming references.mdsmith lsp— the editor surface; an explorer rename firesworkspace/willRenameFiles, which runs the same move engine.