Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,10 +157,12 @@ Windows way in that installs nothing; pipx's `openRepoShape` runs the same flow.
Git Bash runs step 3's bash command and its one line, tested in CI but for a
typed `yes`, which its preflight cannot take there. Printed commands are quoted
for a POSIX shell, so paste them into Git Bash. Only `shape-doctor.py`'s lines,
`setup-project.py`'s family lines and `family.py`'s `NEXT`, `switch -c`, `add`
and `git rm -r --cached` lines are quoted for PowerShell, on Windows.
Off Windows, a path `shape-doctor.py` cannot print as itself is written `$'…'`:
bash, zsh and ksh read that; dash (`/bin/sh` on Debian and Ubuntu) does not.
`setup-project.py`'s family lines and `family.py`'s `NEXT`, `switch -c`, `add`,
`cd` and `git rm -r --cached` lines are quoted for the shell they are printed
in, PowerShell or Git Bash, on Windows.
Off Windows and in Git Bash, a path `shape-doctor.py` cannot print as itself is
written `$'…'`: bash, zsh and ksh read that; dash (`/bin/sh` on Debian and
Ubuntu) does not.

</details>

Expand Down
25 changes: 14 additions & 11 deletions docs/handbook.html
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,7 @@

<!-- reads README.md § preamble: blob ddadd23a162129f7e64fefce899105661440c05c -->

<!-- reads README.md § Starting a project in a new organisation: blob 3d4a01ffadc322f88dfffd9cef50b1c48c58b9b3 -->
<!-- reads README.md § Starting a project in a new organisation: blob e0d2b672082068b2df39ebf54801e068343a14c6 -->

<!-- reads README.md § The three legs: blob 4ec313e8424665e41fc92a4147808e0bd4901d78 -->

Expand Down Expand Up @@ -612,7 +612,7 @@ <h4>The one thing to read before electing it</h4>

<section id="start">

<!-- reads README.md § Starting a project in a new organisation: blob 3d4a01ffadc322f88dfffd9cef50b1c48c58b9b3 -->
<!-- reads README.md § Starting a project in a new organisation: blob e0d2b672082068b2df39ebf54801e068343a14c6 -->

<p class="eyebrow">Setup</p>
<h2>Start here</h2>
Expand Down Expand Up @@ -737,10 +737,13 @@ <h4>The tool offers the rest</h4>
Printed commands are quoted for a POSIX shell, so paste them into Git Bash. Only
<code>shape-doctor.py</code>&rsquo;s lines, <code>setup-project.py</code>&rsquo;s family
lines and <code>family.py</code>&rsquo;s <code>NEXT</code>, <code>switch -c</code>,
<code>add</code> and <code>git rm -r --cached</code> lines are quoted for PowerShell, on
Windows. Off Windows, a path <code>shape-doctor.py</code> cannot print as itself is
written <code>$'&hellip;'</code>: bash, zsh and ksh read that; dash (<code>/bin/sh</code>
on Debian and Ubuntu) does not.</p>
<code>add</code>, <code>cd</code> and <code>git rm -r --cached</code> lines are quoted for
the shell they are printed in, PowerShell or Git Bash, on Windows. A shell started from
inside Git Bash inherits its <code>MSYSTEM</code>, which is how a tool tells Git Bash
apart, so a PowerShell started there is handed Git Bash&rsquo;s quoting, and a cmd.exe
started anywhere else is handed PowerShell&rsquo;s. Off Windows and in Git Bash, a path
<code>shape-doctor.py</code> cannot print as itself is written <code>$'&hellip;'</code>:
bash, zsh and ksh read that; dash (<code>/bin/sh</code> on Debian and Ubuntu) does not.</p>
</div>

</div>
Expand Down Expand Up @@ -1089,7 +1092,7 @@ <h4>What you have afterwards &mdash; the same on every platform</h4>

<section id="partway">

<!-- reads README.md § Starting a project in a new organisation: blob 3d4a01ffadc322f88dfffd9cef50b1c48c58b9b3 -->
<!-- reads README.md § Starting a project in a new organisation: blob e0d2b672082068b2df39ebf54801e068343a14c6 -->

<p class="eyebrow">Setup</p>
<h2>If it fails part-way</h2>
Expand Down Expand Up @@ -1126,7 +1129,7 @@ <h2>If it fails part-way</h2>

<section id="ci">

<!-- reads README.md § Starting a project in a new organisation: blob 3d4a01ffadc322f88dfffd9cef50b1c48c58b9b3 -->
<!-- reads README.md § Starting a project in a new organisation: blob e0d2b672082068b2df39ebf54801e068343a14c6 -->

<!-- reads README.md § The three legs: blob 4ec313e8424665e41fc92a4147808e0bd4901d78 -->

Expand Down Expand Up @@ -1189,7 +1192,7 @@ <h2>Reading private legs in CI</h2>

<section id="legs">

<!-- reads README.md § Starting a project in a new organisation: blob 3d4a01ffadc322f88dfffd9cef50b1c48c58b9b3 -->
<!-- reads README.md § Starting a project in a new organisation: blob e0d2b672082068b2df39ebf54801e068343a14c6 -->

<!-- reads README.md § The three legs: blob 4ec313e8424665e41fc92a4147808e0bd4901d78 -->

Expand Down Expand Up @@ -1445,7 +1448,7 @@ <h3>The InkRouter example</h3>

<section id="doublepin">

<!-- reads README.md § Starting a project in a new organisation: blob 3d4a01ffadc322f88dfffd9cef50b1c48c58b9b3 -->
<!-- reads README.md § Starting a project in a new organisation: blob e0d2b672082068b2df39ebf54801e068343a14c6 -->

<!-- reads README.md § The double pin, and the lockstep invariant: blob 57fffa08ffad5476f8eae73dd3e8494a02e050c9 -->

Expand Down Expand Up @@ -1744,7 +1747,7 @@ <h2>Is this repository compliant? <code>openRepoShape --doctor</code></h2>

<section id="park">

<!-- reads README.md § Starting a project in a new organisation: blob 3d4a01ffadc322f88dfffd9cef50b1c48c58b9b3 -->
<!-- reads README.md § Starting a project in a new organisation: blob e0d2b672082068b2df39ebf54801e068343a14c6 -->

<!-- reads README.md § Carrying in-flight work to another workstation: blob b84512ad39c09085dfe6bf7ea694017f5e28c1dd -->

Expand Down
48 changes: 42 additions & 6 deletions scripts/repo_shape.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,11 @@
- `spelled` and `escape_unencodable_stdout` — how both roots'
`bootstrap.py` print a person's text: what a terminal must not be handed
raw is escaped, and a stdout that cannot encode it never raises (#391).
- `shell_quoted` and `shell_line` — how `scripts/family.py` and a holder's
`scripts/siblings.py` spell a path in a command a person pastes, so that
it reaches the command as ONE argument in `sh` or PowerShell (#486).
- `shell_quoted` and `shell_line` — how `scripts/family.py` spells a path
in a command a person pastes, so that it reaches the command as ONE
argument in `sh` or PowerShell (#486), and
`host_shell_dialect`, which of the two a line printed here is for: Git
Bash on Windows gets `sh`'s (#711).

This file is COPIED into a scaffolded assembly root by `scaffold-project.py`
and its sha256 is recorded in that project's `contracts/shape-pin.yaml`, so an
Expand Down Expand Up @@ -3406,10 +3408,44 @@ def spelled(value: object) -> str:
POWERSHELL_SINGLE_QUOTES = frozenset("'‘’‚‛")


def host_shell_dialect(name: str | None = None, environ=None) -> str:
"""Which shell a line printed on this host is quoted for, as the
`platform` `shell_quoted` takes: `"nt"` for PowerShell's dialect, and
`"posix"` for `sh`'s (#711). `name` and `environ` are `os.name` and
`os.environ` when None, so a test can ask about any host from any host.

THE RULE. A Windows host (`os.name` is `nt`) is PowerShell's, unless its
environment has a non-empty `MSYSTEM`: then the tool was run from Git
Bash, or another MSYS2 shell, which reads a line as `sh` does, so the
line gets `sh`'s quoting. Every other host is `sh`'s. It is asked each
time a line is quoted, not once at import, so the answer is the
environment's the line is printed in.

WHY `MSYSTEM`. Under Git Bash `os.name` is `nt`, as it is under
PowerShell, because the Python Git Bash runs is the same Windows Python,
so `os.name` cannot tell the two shells apart. Git Bash and every MSYS2
shell set `MSYSTEM` (`MINGW64`, `UCRT64`, `CLANGARM64`, `MSYS`, ...) and
hand it to every program they start; PowerShell and cmd.exe do not set
it. An EMPTY `MSYSTEM` names no MSYS2 shell, so it is not Git Bash.

WHAT IT DOES NOT COVER. A PowerShell or cmd.exe started from inside Git
Bash inherits `MSYSTEM`, so a tool run there is answered `sh`'s and
prints `sh`'s quoting into PowerShell: nothing the tool can read tells
that PowerShell from Git Bash, and this does not try. It answers the
quoting and the shell syntax around a word, and nothing about the
machine: `PYTHON` stays `python` under Git Bash, which runs the Windows
Python, and a path keeps its Windows spelling.
"""
name = os.name if name is None else name
environ = os.environ if environ is None else environ
return "nt" if name == "nt" and not environ.get("MSYSTEM") else "posix"


def shell_quoted(value: object, platform: str | None = None) -> str:
"""`value` as ONE word of a command a person pastes into the shell of
`platform` (`os.name` when None): bare where that shell reads every
character of it as itself, else whole, in single quotes (#486).
`platform` (`host_shell_dialect()` when None, #711): bare where that
shell reads every character of it as itself, else whole, in single
quotes (#486).

THE DOCTOR'S RULE, `shape-doctor.py`'s `quote_arg`, which has quoted
every path its NEXT lines print since #101/#102: `sh` and PowerShell are
Expand Down Expand Up @@ -3438,7 +3474,7 @@ def shell_quoted(value: object, platform: str | None = None) -> str:
`quote_arg` still leaves it bare: a known gap, named where #486 lands.
"""
text = str(value)
windows = (os.name if platform is None else platform) == "nt"
windows = (host_shell_dialect() if platform is None else platform) == "nt"
bare = SHELL_BARE_NT if windows else SHELL_BARE_POSIX
if not text.startswith("=") and bare.fullmatch(text):
return text
Expand Down
49 changes: 36 additions & 13 deletions setup-project.py
Original file line number Diff line number Diff line change
Expand Up @@ -2687,32 +2687,55 @@ def clone_and_bootstrap(opts: Options, org: str, landing: Landing) -> dict:
#: `SHELL_BARE_POSIX`. A backslash is not in it, so a Windows path is quoted.
SHELL_BARE_POSIX = re.compile(r"[A-Za-z0-9_./:@%+=,-]+")

#: A word this host's shell reads as itself, needing no quotes:
#: `scripts/repo_shape.py`'s `SHELL_BARE_NT` on Windows, else its
#: `SHELL_BARE_POSIX`. This file runs before any checkout of the standard
#: A word PowerShell reads as itself: `scripts/repo_shape.py`'s
#: `SHELL_BARE_NT`. This file runs before any checkout of the standard
#: exists, so it carries its own copy of the rule, and
#: `tests/test_family_printed_quoting.py` holds the two to one answer.
SHELL_BARE = re.compile(r"[A-Za-z0-9_./:%+=\\-]+" if os.name == "nt"
else SHELL_BARE_POSIX.pattern)
#: `tests/test_git_bash_dialect.py` holds the two to one answer on every
#: host, as `tests/test_family_printed_quoting.py` does on this one.
SHELL_BARE_NT = re.compile(r"[A-Za-z0-9_./:%+=\\-]+")


def host_shell_dialect(name=None, environ=None) -> str:
"""`scripts/repo_shape.py`'s `host_shell_dialect`, as a copy, because
this file runs before any checkout of the standard exists (#711):
`"nt"`, PowerShell's dialect, on a Windows host whose environment has
no `MSYSTEM` or an empty one, and `"posix"`, `sh`'s, everywhere else,
Git Bash included. `name` and `environ` are `os.name` and `os.environ`
when None. Its docstring there says why `MSYSTEM`, and what it does not
cover: a PowerShell started from inside Git Bash inherits `MSYSTEM` and
is answered `sh`'s. `tests/test_git_bash_dialect.py` holds the two
copies to one answer."""
name = os.name if name is None else name
environ = os.environ if environ is None else environ
return "nt" if name == "nt" and not environ.get("MSYSTEM") else "posix"


def shell_word(value, posix: bool = False) -> str:
"""`value` as ONE word of a command pasted into this host's shell: bare
where every character is itself to it, else in single quotes, doubled
inside for PowerShell, closed-escaped-reopened for `sh` (#486). A
leading `=` is quoted too, which zsh would expand.
"""`value` as ONE word of a command pasted into this host's shell, the
one `host_shell_dialect` answers (#711): bare where every character is
itself to it, else in single quotes, doubled inside for PowerShell,
closed-escaped-reopened for `sh` (#486). A leading `=` is quoted too,
which zsh would expand.

ASKED AT EACH CALL, NOT AT IMPORT. The alphabet a word may stay bare in
and the escape it is quoted with are one answer, taken from the
environment the line is printed in, where `MSYSTEM` says Git Bash. The
alphabet used to be fixed at import (`SHELL_BARE`, from `os.name`) and
the escape asked `os.name` at each call, two reads of one question.

`posix` spells it for `sh` on EVERY host, Windows too: Git Bash reads
the backslash of a bare `C:\\work` as an escape and an apostrophe doubled
for PowerShell as two quotes, and #532 rules the printed commands
POSIX-only. The lines #538 adds or changes pass it, as the scaffold's
NEXT STEPS clone does (`shell_line(..., "posix")`); the family lines
(`family_next_command`) are #532's and still take this host's dialect."""
(`family_next_command`) are #532's and take the host's dialect, which
under Git Bash is `sh`'s (#711, L2-29)."""
text = str(value)
bare = SHELL_BARE_POSIX if posix else SHELL_BARE
windows = not posix and host_shell_dialect() == "nt"
bare = SHELL_BARE_NT if windows else SHELL_BARE_POSIX
if not text.startswith("=") and bare.fullmatch(text):
return text
if os.name == "nt" and not posix:
if windows:
return "'" + re.sub("(['\u2018\u2019\u201a\u201b])", r"\1\1",
text) + "'"
return "'" + text.replace("'", "'\\''") + "'"
Expand Down
Loading
Loading