Repository navigation
install.sh and openRepoShape: a Windows-spelled $OPENREPOSHAPE_BIN_DIR is respelled or refused before install.sh's fetch, and the PATH note quotes the directory it prints and refuses one holding : or a newline (#667) - #726
Conversation
…IR` is respelled with `cygpath -u` or refused before install.sh's fetch, and the PATH note single-quotes a directory bash would still read, and `--install` refuses one holding `:` or a newline, so install.sh names `/d/tools/bin` as the command does and the `export PATH` line does what it says (#667) #649's two leftovers, both from #656's review. install.sh read `$OPENREPOSHAPE_BIN_DIR` as typed, so with `D:\tools\bin` its `preflight:` and `done:` lines (and `install:` for an old release) said `D:\tools\bin/openRepoShape` while the command's own lines said `/d/tools/bin/openRepoShape`, and where no `cygpath` answers the command's `--install` refused (exit 2) only after install.sh had fetched it. install.sh now carries a copy of the command's rule where it reads the variable, before the symlink check and the fetch: a value that starts `[A-Za-z]:/` or `[A-Za-z]:\` is spelled with `cygpath -u --`, and is refused with exit 2 where no `cygpath` answers, fails, or answers a drive spelling again. The refusal does not echo the value. The command's PATH note printed `export PATH="$dir:$PATH"` with the directory between double quotes as it was, so `a"b$HOME c` printed a line `eval` could not parse, and a `$`, a backquote or a `\` in a name was read, not kept. `export_path_line` keeps today's line byte for byte for a directory that double quotes hold (a space and an apostrophe included) and, for one holding a `"`, `$`, backquote, `\` or `!` (bash reads that at a prompt as history), prints it between single quotes with `'\''` for an apostrophe. `--install` also refuses a directory holding a `:` or a newline, which no `$PATH` line can name, before anything is placed; the newline's refusal does not echo it. Tests feed one value through install.sh and through the command, in each spelling and each state of `cygpath`, and pin every `install:`, `preflight:` and `done:` line; the PATH note is checked as printed, by `eval` and by `bash -i`. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-5, 2026-10-16T17:09Z) @codex review |
Reviewer's GuideThe PR makes Windows-spelled bin directories consistent between install.sh and openRepoShape, validates directories before installation, safely quotes generated PATH guidance, and adds extensive tests covering conversion behavior, shell metacharacters, refusal ordering, and installer output. Sequence diagram for safe bin-directory installationsequenceDiagram
participant User
participant Installer as install.sh
participant CyPath as cygpath
participant Network
participant Command as openRepoShape
User->>Installer: run installer
Installer->>Installer: read OPENREPOSHAPE_BIN_DIR
alt Windows spelling
Installer->>CyPath: cygpath -u -- bin_dir
alt path cannot be respelled
CyPath-->>Installer: failure or Windows spelling
Installer-->>User: refuse with exit 2
else path respelled
CyPath-->>Installer: Unix-style directory
end
else shell-style directory
Installer->>Installer: keep directory
end
alt valid directory
Installer->>Installer: check symlink
Installer->>Network: fetch release
Installer->>Command: run --install
Command-->>Installer: installation result
Installer-->>User: report one consistent directory
end
Sequence diagram for safe PATH guidance generationsequenceDiagram
participant User
participant Command as openRepoShape
participant Shell
User->>Command: run --install
Command->>Command: export_path_line(dir)
alt dir contains shell-special character
Command-->>User: export PATH='<dir>':"$PATH"
else dir is safe for double quotes
Command-->>User: export PATH="<dir>:$PATH"
end
User->>Shell: paste PATH guidance
Shell-->>User: PATH contains exact directory and prior PATH
Flow diagram for bin-directory validationflowchart TD
A[Read bin directory] --> B{Contains newline?}
B -->|yes| C[Refuse with exit 2]
B -->|no| D{Windows drive spelling?}
D -->|yes| E[Run cygpath -u --]
E --> F{Successfully respelled?}
F -->|no| C
F -->|yes| G[Use respelled directory]
D -->|no| H[Use original directory]
G --> I{Contains colon?}
H --> I
I -->|yes| C
I -->|no| J[Install command and print safe PATH guidance]
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a3cde999c3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| die "\$OPENREPOSHAPE_BIN_DIR is spelled for Windows, and no cygpath here spells it the way bash does, so the directory could not go on \$PATH: the drive's colon splits it. Nothing was installed. Set \$OPENREPOSHAPE_BIN_DIR to the directory as this shell spells it, then run this line again." | ||
| ;; | ||
| esac | ||
| bin_dir="$spelled" |
There was a problem hiding this comment.
Pass the respelled directory to the downloaded launcher
When a Windows-spelled directory is successfully converted, bin_dir changes only as a local shell variable; OPENREPOSHAPE_BIN_DIR in the environment remains unchanged when the downloaded launcher runs. Consequently, installing a named release from before the launcher's own cygpath fix still receives and reports the raw D:\... spelling, prints its broken PATH instruction, and on shells where the two spellings are not aliases may install somewhere other than the normalized $target that this script subsequently executes. Pass the normalized value in the environment of the downloaded launcher so the installer's documented compatibility with older named releases also gets this fix.
Useful? React with 👍 / 👎.
…es every reason THIS checkout cannot answer, and the notes are current to main bf1f099 with the 25 pull requests since de92614 (#563, #683) - W683 and RV-f's N4 on #742: README's exit-3 list (Keeping a project's shape current, :1208-1220) and its handbook twin (`doctor`) named only a pin whose commit this checkout does not carry. They now say, as shape-doctor.py's EXIT CODES does: a pin's commit it lacks, a partial clone missing what is compared (#683), a read that fails (`update-upstream-unreadable`), a crashed validator of its own (#278, #410), or its own naming policy not loading (#678). +1 line, with tests/readme-cap.d/683.txt; README.md is 1474, its cap (1422 + 2 + 5 + 12 + 22 + 10 + 1). The two `Keeping a project's shape current` markers move: `doctor` text and marker, `current` marker only. - The notes, "What changed since v1.0.1", one entry per pull request from `git log --first-parent de92614..bf1f099`, each from its squash commit and the issues GitHub says it closes: - adopt-project.py: #699, #719, #749, #727, #717, #728 (with #621's hint), #743 + #720 + #718, #722, #721; - update-shape.py, bump-leg.py and a project's own scripts: #702 (with the pinned validators), #634; - scaffold, setup, family and the shared readers: #725, #671, #744; - shape-doctor.py: #742, #701, #737, #751, #736, #734, #700 + #705, #740; - install, CI and the docs: the exit-3 sentence above. - Counts (99 pull requests since bfed143; 109 of 135 milestone issues closed at 10:28Z), the "current to main" sha and the open list (26) are at main bf1f099, still placeholders for tag time. Left for round B: lane 5's eight lines (#630 issuecomment-6227129879), whose pull requests #726, #747, #682, #724, #746, #745 and #738 are open drafts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days. You can request another review in 12 hours and 9 minutes by commenting |
main gained #706 (#664), #703 (#696), #670 (#646), #682 (#648), #726 (#667), #730 (#666) and #724 (#665) since bf1f099. No conflict: #682's README sentence and its handbook twin (Families) merged as text, with #682's own Families marker, and every marker reads current. README.md is 1474 lines, its cap; AGENTS.md 494, its cap; `render-cli-reference.py --check` is ok. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d A3 (three SHOULDs, four nits), and notes entries for the seven pull requests landed since bf1f099, current to main 2000488 (#563, #683) Round A4, from lane 2's review (#630 issuecomment-6233033833, READY): - SHOULD 1: the doctor's exit-3 list names "a validator of its own that crashed or could not be run", so the case of one that cannot be given its `PYTHONPATH` (shape-doctor.py's EXIT CODES, #278) is listed too: README.md:1208-1222, its handbook twin (`doctor`) and the notes' docs line. The paragraph re-wraps to one more line: tests/readme-cap.d/683.txt is +2, and its text now says which causes the EXIT CODES block names and that #742 added the partial clone and #701 the naming policy. README.md is 1475 lines, its cap (1422 + 2 + 5 + 12 + 22 + 10 + 2). Both `Keeping a project's shape current` markers move to c728c970: `doctor` text and marker, `current` marker only. - SHOULD 2: #727's entry names the leg description, whose em dash stopped every `execute` against GitHub at the first leg. - SHOULD 3: #749's entry names `source-ambiguous`, for an `org/repo` that is also a directory here. - Nits: the doctor section's three near-duplicate pairs are merged (#636 with #737, #632 with #700 and #705, #586 with #736); #718 has its own bullet; #634's entry names `git_out`'s spelling for every caller and #744's the gate's `--link-source-label` and `--link-remedy`; the header comment says 2026-10-17 at main 2000488. Wave B1, `git log --first-parent bf1f099..2000488`: #706 (#664), #703 (#696), #670 (#646), #682 (#648), #726 (#667), #730 (#666) and #724 (#665), each from its squash commit; #726's and #682's from lane 5's lines (#630 issuecomment-6227129879), #682's with the unmerged `.gitmodules` its last round added, and #724's with lane 5's note that v1.0.0's test-hygiene line on the generator's `.pyc` files closes. The counts (106 pull requests; 116 of 135 milestone issues closed at 11:53Z), "current to main" and the open list (19) are at main 2000488, still placeholders. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…holding a control character before they name it, and install.sh refuses a `:` before its fetch, so a carriage return or an escape is no longer echoed as typed (#760) (#804) Against main. #726 landed as `dcf62ed`, and this branch is restacked onto main as `7e4181c0`: it merges #726's final head `a22b94c4`, then main, taking this branch's side in the four files #726 also changed. Over main it carries exactly its own two commits: `git diff a3cde99 efd061a` and `git diff origin/main 7e4181c` have the same patch-id. #667's two checklist candidates, with rv667's NIT 2. Two commits on a3cde99: **7569ffa8**, and the fix round's **efd061a7**. Against a3cde99, `git diff --numstat`: `install.sh` +40/-3, `openRepoShape` +30/-16, `tests/test_install_sh.py` +132/-2, `tests/test_openreposhape_command.py` +176/-21, and nothing else. README, the handbook, `AGENTS.md` and `docs/cli.md` are unchanged (`render-cli-reference.py --check`: ok): no documented sentence names these refusals, and neither file is a pinned shape copy, so no `upstream-changed` row follows. One new commit, **efd061a7**, on 7569ffa, pushed to `fix/760-bin-dir-control-chars` (a fast-forward). The PR is now two commits on a3cde99. `git diff --numstat 7569ffa efd061a`: `install.sh` +12/-7, `openRepoShape` +14/-11, `tests/test_install_sh.py` +34/-29, `tests/test_openreposhape_command.py` +61/-35, and nothing else. Against a3cde99 the PR is `install.sh` +40/-3, `openRepoShape` +30/-16, `tests/test_install_sh.py` +132/-2, `tests/test_openreposhape_command.py` +176/-21. README, the handbook, `AGENTS.md` and `docs/cli.md` are still unchanged. The red Linux run on 7569ffa was #645's `copytree` race (`shutil.Error` in `test_doctor_pin_restore_merges.py`'s setup), which #713 fixed on main. It is not this PR's fault, and nothing here touches it. ### Finding 1 (should-fix): taken. A C1 control passed the arm under a UTF-8 locale when the value also held a byte that was not UTF-8 **Reproduced first, on 7569ffa, with bash 5.2.21.** `LC_ALL=C.UTF-8`, `$OPENREPOSHAPE_BIN_DIR` set to `<dir>/caf`, the byte 0xE9, U+009B, `31mFORGED/bin`, and `openRepoShape --install`: exit 0, the command placed, and the value printed raw in four lines (`od -c` of the output shows `c a f 351 302 233 3 1 m F O R G E D`). It is lane 1's reproduction, and it is the failure the finding names. Cause: for a value that is not valid in the locale's multibyte encoding, bash matches byte by byte, and read one byte at a time U+009B's two bytes (`0xC2 0x9B`) are not controls, as under `C`. **The fix is lane 1's, tested by them, in both files** (`install.sh`'s `main` and `openRepoShape`'s `install_commands`), in place of the `case`: ```sh if (LC_ALL=C; case "$bin_dir" in *[[:cntrl:]]* | *$'\302'[$'\200'-$'\237']*) exit 0 ;; *) exit 1 ;; esac); then die "…unchanged…" fi ``` The command's has `"$dir"`. Under `LC_ALL=C` the class is the ASCII controls and DEL, byte by byte, and the second pattern names the two bytes of each of U+0080 to U+009F (`0xC2`, then `0x80` to `0x9F`). The arm is the same in every locale. The subshell leaves the locale of every later line as it was. I added a `*) exit 1 ;;` arm so the construct has a default case (finding 2). Both files' comments now say what the check does, and no longer say which C1 controls match is the locale's. **Measured, command `--install` (bash 5.2.21; each cell is exit, whether the command was placed, whether the value reached the output):** | value | locale | 7569ffa | efd061a | |---|---|---|---| | U+009B | `C`, `POSIX` | 0, placed, in 4 lines | 2, nothing placed, nothing echoed | | U+009B | `C.UTF-8` | 2 | 2 | | U+0080, U+009F | `C`, `POSIX` | 0, placed | 2, nothing placed | | U+0080, U+009F | `C.UTF-8` | 2 | 2 | | 0xE9 then U+009B then `FORGED` | `C`, `POSIX`, `C.UTF-8` | 0, placed, in 4 lines | 2, nothing placed, nothing echoed | | a `:`, 0xE9, U+009B | all three | 2, the colon refusal names the directory, the C1 control in its line | 2, the control-character refusal, nothing of the value | | an e with an acute accent, U+00A0 | all three | placed | placed | | a lone 0x9B byte | all three | placed | placed (see the checklist) | | U+2028 | `C`, `POSIX` | placed | placed | | U+2028 | `C.UTF-8` | **2** | **placed** (see below) | **Measured, install.sh with a fake `curl` that logs each request and exits 22, a version named:** | value | locale | 7569ffa | efd061a | |---|---|---|---| | U+009B, U+0080, U+009F | `C` | exit 2 after 1 request | exit 2, 0 requests | | 0xE9 then U+009B | `C`, `C.UTF-8` | exit 2 after 1 request | exit 2, 0 requests | | a `:`, 0xE9, U+009B | `C`, `C.UTF-8` | exit 2, 0 requests, the colon refusal holds the C1 control | exit 2, 0 requests, nothing of the value | | an e with an acute accent, U+00A0 | `C`, `C.UTF-8` | 1 request (the value is accepted) | 1 request (the value is accepted) | **Side effect, as lane 1 said: U+2028 and U+2029 are refused in no locale now.** Only a UTF-8 locale's `[[:cntrl:]]` matched them, and no one chose that. They are not controls a terminal acts on, and a line that holds one prints as it is. `NOT_CONTROLS` places both under `C` and `C.UTF-8`, through both files, so the decision is held and not left to the locale. If a ruling wants them refused, `*$'\342\200'[$'\250\251']*` joins the pattern in both files and the two rows turn into refusals. **What the locale still does, measured:** over every byte string of up to three bytes from a set of fourteen (2955 strings: ASCII, ESC, DEL, 0x80, 0x85, 0x9B, 0x9F, 0xA0, 0xA9, 0xC2, 0xC3, 0xE2, 0xE9), under `C`, `POSIX` and `C.UTF-8`, the new arm gives the same answer in all three locales, and the arm without `LC_ALL=C;` differs from it on exactly one string: U+2029 (0xE2 0x80 0xA9) under `C.UTF-8`. So on glibc the locale's one observable effect is those two characters, and that is what pins `LC_ALL=C` in the tests (see the mutants). It stays in the code as lane 1 specified it, so the match is byte by byte by construction on a libc whose single-byte `iscntrl` says otherwise for 0x80 to 0x9F in a UTF-8 locale. macOS's was not measured. ### Finding 2 (nit, two SonarCloud S131 issues): not taken, partly cleared The colon `case` in `install.sh` has no `*)` arm and keeps its S131, as nine others open in that file do. The control check's `case` now has one, so its S131 should go. That is expected from the code, not measured: SonarCloud's run on this push says. ### Finding 3 (info): #804 depends on #726 #726 has landed on main as dcf62ed. This branch still descends from a3cde99, so `git merge-tree --write-tree origin/main efd061a` (origin/main 40e678e) conflicts in `install.sh`, `openRepoShape` and both test files, as lane 1 found for a cherry-pick. I did not restack: the coordinator retargets and restacks by the stacked-PR procedure. ### Ruling on the colon refusal naming the directory (lane 1 agreed; record it when #760 closes) Lane 1 agreed that the colon refusal names the directory, and asked that it be recorded on #760 at close: #760's Verify line says "neither stream holds the value" for all four values, colon included, and the colon's is named, by a choice every `$HOME` default needs. With this commit the premise is whole: a value that reaches the colon refusal holds no ASCII control character, DEL or C1 control, in any locale and whatever else it holds, so naming it cannot forge a line or reach the terminal. ### Tests at efd061a (every run through `run9.sh`: gh stub first on `PATH`, bogus token, empty `GH_CONFIG_DIR`; dirty=0) Logs are in `$Q/v116/f804/` (`$Q` is the lane's scratchpad). | Run | Result | Log | |---|---|---| | The new and changed cases (`-k "c1_control or unprinted_letter"`) | **36 passed** (18 in each file) | `new-final.log` | | `test_openreposhape_command.py`, `test_install_sh.py`, `test_install_sh_filter_repo.py`, `test_openreposhape_package.py`, `test_repo_hygiene.py`, `test_agents_md_codes.py`, `test_cli_reference.py` | **478 passed**, 103.92s | `runA-final.log` | | the three files `tests-git-bash` runs, with every `Path.write_text` writing CRLF (the plugin that wave 7 used) | **274 passed**, 65.38s | `runC-final.log` | | `test_release_workflow.py`, `test_doctor_validator_rows.py`, `test_doctor_machine_row.py` | **93 passed**, 50.72s | `runB-final.log` | | `test_shape_doctor.py -k "shim or installed_doctor or shipped_doctor or refused_doctor or signal"` (it runs the launcher's `--doctor`) | **20 passed**, 388 deselected | `runD-final.log` | - **What changed in the tests.** - The C1 case, which skipped where no UTF-8 locale read U+009B as a control, is gone with its helper `utf8_locale_with_c1_controls`. In its place a table of five values (`C1_BIN_DIRS`) runs under both `C` and `C.UTF-8`, through the command and through install.sh. The values are U+009B, U+0080, U+009F, 0xE9 then U+009B, and a `:` beside that. Nothing skips for want of a locale. - The placed-name test takes four names (`NOT_CONTROLS`): an e with an acute accent, U+00A0 (the byte after the range's last), U+2028 and U+2029, under both locales, through both files. The installer's version looks for its `preflight:` line among lines split at a newline only, because `str.splitlines` also splits at U+2028 and U+2029. - A lone 0xE9 is `chr(0xDCE9)`, which reaches the child as that byte through `surrogateescape`. No escape is typed into any file, and `body-guard.py` is clean on the four files. - **On the base.** The new and changed cases are 36: **18 fail on 7569ffa's code and 18 pass** (`new-base2.log`). The 18 that pass are guards: U+009B, U+0080 and U+009F under `C.UTF-8` (6, which the head already refused), and the placed names under `C` and the e and U+00A0 under both (12). Of the 18 that fail, 8 surface as a `UnicodeDecodeError` raised inside `subprocess.run` (the head echoes the lone 0xE9, and `run_cmd` and `Net.run` decode strictly) and 10 as assertions. That is a rough failure message and no more: decoding with `errors="surrogateescape"` in those two helpers would turn the 8 into assertions, and I left it, as the round is one commit and no mutant of the helpers can fail on the fixed code. Over a3cde99's code, the larger selection of #760's cases (`-k` of the refusal, installer, default-directory, control, letter and C1 tests, 62 cases) gives **42 failed, 20 passed** (`new-base-a3cde999.log`); over 7569ffa's it gives 18 failed, 44 passed (`new-base-7569ffa8.log`). ### Mutants of the two new hunks, and the earlier commit's rows against the new arm Each runs in a scratch clone against 95 selected cases (the refusal, installer, default-directory, control, letter, C1, Windows-spelling and symlink cases), one edit at a time, restored after. "Failed" is how many of the 95 failed. **All 28 are caught.** Logs: `mut2/` and `mut3/`; the runner is `mutate2.py`; the counts are in `mutants2.out` and `mutants3.out`. | Mutation | Command | install.sh | Where it fails | |---|---|---|---| | no C1 bytes in the pattern | 10 | 10 | every C1 value, under both locales, in the file mutated | | `LC_ALL=C;` dropped | 4 | 2 | U+2028 and U+2029 under `C.UTF-8` (the installer's test runs the command too) | | `[[:cntrl:]]` dropped, C1 bytes kept | 17 | 9 | carriage return, tab, DEL, escape and the drive and colon rows | | range starts one late (`\201`) | 2 | 2 | U+0080, both locales | | range ends one early (`\236`) | 2 | 2 | U+009F, both locales | | range ends one late (`\240`) | 4 | 2 | U+00A0 is refused (the placed-name test) | | lead byte `\303` for `\302` | 10 | 10 | every C1 value | | the whole arm removed | 27 | 19 | every control value | | arm narrowed to a newline | 23 | 17 | every control but a newline | | `[![:print:]]` for the pattern | 16 | 8 | the e with an acute accent under `C`, and more | | refusal echoes the value | 27 | 19 | every control value | | arm after the drive arm (command) | 4 | | the two drive rows (both files) | | arm after the colon arm | 8 | 5 | the colon and the drive rows, the colon beside a C1 control | | arm after the symlink loop (command) / check (install.sh) | 9 | 6 | the link case, the colon and drive rows | | arm after the release lookup (install.sh) | | 19 | a request was made, for every control value | ### Not checked, and known limits - **bash 3.2.57** is not here. Lane 1 built it and measured the same table on it, and the pattern is theirs. I did not run it. - **macOS and other libcs.** `LC_ALL=C` is what is meant to hold there. Not run. - **A GBK or GB18030 locale.** The bytes `0xC2 0x80` to `0xC2 0x9F` are 32 hanzi there (`0xC2 0x9B` decodes to one with Python's `gbk` codec), and the arm now refuses a directory that holds one as "a control character", where `[[:cntrl:]]` placed it. Measured with Python's codecs only: no such locale is installed here. Big5, Shift_JIS (`cp932`), EUC-KR and EUC-JP read those bytes as invalid. A candidate, not a fix: it needs the locale's charset asked first. - **Windows (Git Bash).** The C1 and placed-name tests skip there, as before; the three files `tests-git-bash` runs pass with CRLF `write_text` emulated (274). **1. The command refuses any control character in the directory, first** (`install_commands`, `openRepoShape`). - Before: only a newline was refused first. Measured on a3cde99: `$OPENREPOSHAPE_BIN_DIR` set to `<dir>/bin`, a carriage return, then `FORGED` placed the command, exit 0, and printed `…/bin^MFORGED is not on $PATH`. An escape sequence did the same (`…/bin^[[31mFORGED`). The value reached four lines: `installed at`, `placed in`, `is not on $PATH` and the `export PATH` line. - Now: the arm is `*[[:cntrl:]]*` and the two bytes of each C1 control (U+0080 to U+009F), run under `LC_ALL=C`, still first, before the Windows spelling and the colon. Its refusal says "holds a control character, such as a newline, a carriage return, a tab or an escape" and nothing of the value, so no refusal after it can echo an ASCII control character, DEL or a C1 control, in any locale and whatever else the value holds, that forges a line or reaches the terminal. Exit 2, nothing placed. **2. install.sh carries both arms and refuses before any request** (`main`, `install.sh`). - Before: a `:` or a newline reached the fetch. install.sh fetched the launcher, ran `--install`, and said only "the v9.9.9 openRepoShape's --install refused (exit 2)"; a release older than #667 had no such refusal and placed the command under the directory. Measured with a fake `curl` that logs its calls, version named: a3cde99's install.sh made one request for each of a colon, a newline, a carriage return and an escape; this head makes none. - Now, copied from the command as #667 copied the drive-letter `case` (install.sh arrives alone down a pipe): the control-character arm right after the variable is read, before the Windows respelling; the colon arm after the respelling, as the command orders them. Both are before the symlink check and every line that names the directory. - The control-character refusal never names the directory. The colon refusal names it, as the command's does (see decisions). - **`[[:cntrl:]]`, not `[![:print:]]`**, as the issue directs. `[![:print:]]` is what `checked_repo` uses for `$OPENREPOSHAPE_REPO`, but under `LC_ALL=C` the two bytes of an e with an acute accent are not printable, so it would refuse a directory called `caf` and that letter. A test places that directory under `C` and under `C.UTF-8`, through both files; the mutants "`[![:print:]]`" fail it. - **A C1 control is refused in every locale.** The check runs under `LC_ALL=C` and names U+0080 to U+009F's two bytes (`\302`, then `\200` to `\237`), whatever else the value holds; U+2028 and U+2029 are refused in none. Left to the locale, `[[:cntrl:]]` matches a C1 control only in a value that is valid UTF-8 (bash 5.2.21, Linux): U+009B alone is refused under `C.UTF-8` and passes under `C` and `POSIX`, and one byte that is not UTF-8 before it (`caf`, 0xE9, U+009B) makes bash match byte by byte, so it passes under `C.UTF-8` too (lane 1 measured the same on bash 3.2.57). The tests run five C1 values under `C` and `C.UTF-8`, through both files, and place U+00A0, U+2028 and U+2029 beside the e with an acute accent. - **A tab and DEL are refused too.** They are control characters, the issue says every one, and no `$PATH` line prints either as it is. - **The colon refusal names the directory; the control-character one never does.** The issue's Verify says "neither stream holds the value" for all four values, but the command's colon refusal (#726) names `'$dir'`, and a test of #726's pins that for the `$HOME` default, where the person typed nothing and needs the directory named. A colon value holds no ASCII control character, DEL or C1 control by then, in any locale, so naming it cannot forge a line or reach the terminal. Lane 1 agreed with this ruling (2026-10-17T11:06:51Z) and asked that it be recorded when #760 closes: the issue's Verify line says "neither stream holds the value" for all four values, and the colon's is named. install.sh's colon refusal does the same. The tests hold the control values to "no `forged` tail, no escape, DEL or C1 in either stream", and the colon values to "named, in quotes". If the coordinator rules that a colon must go unnamed too, it is one message in each file and the same assertion. - **The order is held, not just the width.** A drive spelling with a newline or an escape must be refused as a control character (not as "spelled for Windows" when no `cygpath` answers), a colon with an escape must be refused as a control character, and a link in the directory must not make the symlink refusal print the value. **The figures in this section are 7569ffa's.** At efd061a they are the fix round's table above: 36 new and changed cases, 478 passed in the seven files, 274 passed with CRLF `write_text`, and 93 and 20 passed in the others. The base figures are 18 failed and 18 passed over 7569ffa's code, and 42 failed and 20 passed over a3cde99's for the 62-case selection. 31 cases in the new and extended tests (26 are new; the command's five older refusal cases run again in the same test), run with the one existing test the selection matches: 32 passed in 1.75s on the head, dirty=0. - `tests/test_openreposhape_command.py` (15): the refusal over 11 values (a colon, two directories, a drive-relative name, a newline, a carriage return, an escape, a tab, DEL, a drive spelling with a newline, a drive spelling with an escape, a colon with an escape; the first five were 5 cases, six are new), a letter under `C` and `C.UTF-8`, U+009B under a UTF-8 locale, and a link in a directory holding a carriage return. A shared `assert_a_bin_dir_is_refused` holds exit 2, the wording, the ending, and that no `forged` tail, escape, DEL or C1 reaches either stream. - `tests/test_install_sh.py` (16): the same 11 values through install.sh and the command, in one test (exit 2 for both, the fake `curl` records no call, nothing placed or made in either world, `$TMPDIR` empty), the `$HOME` default with a colon, the symlink case, a letter under both locales (placed, named as typed), U+009B under a UTF-8 locale. - **On the base a3cde99** (the head's two test files over the base's code): `24 failed, 8 passed` in the 32. The 8 are the command's three colon cases and the `$HOME` default (the base already refuses them), and the letter under both locales through both files (the base places it; they guard the width). Of the 24, 22 fail on behaviour (a request was made, or the value was placed and echoed, or the refusal came from the wrong arm); the command's `a-newline` and `a-drive-spelling-and-a-newline` fail only on the refusal's new wording. - **On the head 7569ffa, dirty=0:** - `tests/test_openreposhape_command.py tests/test_install_sh.py tests/test_install_sh_filter_repo.py tests/test_openreposhape_package.py`: **302 passed** in 68.90s. - `tests/test_repo_hygiene.py tests/test_agents_md_codes.py tests/test_cli_reference.py tests/test_setup_by_hand_commands.py tests/test_readme_windows_quoting_sentence.py tests/test_gh_guard.py tests/test_workflow_concurrency.py tests/test_release_workflow.py tests/test_readme_sections.py`: **297 passed** in 38.97s. - `tests/test_shape_doctor.py -k "shim or installed_doctor or shipped_doctor or refused_doctor"` (it runs the launcher's `--doctor`): **20 passed**, 388 deselected. - **The three files `tests-git-bash` runs, with Windows' `write_text` emulated** (a plugin makes every `Path.write_text` write CRLF): **244 passed** in 52.64s. A probe test takes the plugin's effect: 1 passed with it, 1 failed without. - **install.sh, version named, each of a colon, a newline, a carriage return, an escape:** a3cde99: exit 2 after 1 request (the launcher's fetch), refused by the fetch's error or by the command. This head: exit 2, 0 requests, install.sh's own refusal (the colon one names the directory; the others name nothing of it). - **The command, `--install`:** a3cde99: a carriage return and an escape: exit 0, the command placed under the directory, the value in four lines. A newline: exit 2. This head: exit 2 for all three, nothing placed. - **Needs Windows or another system to be real:** none of these rows. See below for what was not run. The fix round above adds 14 mutants of the new pattern line and 4 order mutants, and re-ran this table's rows against the new arm (the whole arm removed, narrowed to a newline, `[![:print:]]`, the refusal echoing, and the two later placements). Each is still caught. Each row changes one arm or hunk in a throwaway clone and runs the new cases and #726's drive and symlink cases (65 selected). "Failed" is how many of the 65 failed. | Mutation | Failed | Where | |---|---|---| | command: no control-character arm | 18 | the command's cases and install.sh's, which runs it | | command: arm narrowed back to a newline | 14 | carriage return, escape, tab, DEL (both files) | | command: `[![:print:]]` for `[[:cntrl:]]` | 2 | the letter under `C` (both files) | | command: arm after the drive arm | 4 | the two drive spelling cases (both files) | | command: arm after the colon arm | 6 | colon with an escape, the two drive cases | | command: arm after the symlink loop | 7 | the link case, colon with an escape, the two drive cases | | command: refusal echoes the value | 18 | every control value | | install.sh: no control-character arm | 10 | the link case, U+009B, the control values | | install.sh: arm narrowed to a newline | 8 | carriage return, escape, tab, DEL, the link case, U+009B | | install.sh: `[![:print:]]` | 1 | the letter under `C` | | install.sh: arm after the drive block | 2 | the two drive spelling cases | | install.sh: arm after the symlink check | 4 | the link case, colon with an escape, the two drive cases | | install.sh: arm after the release lookup | 10 | a request was made, for every control value; and the link refusal named it | | install.sh: refusal echoes the value | 10 | every control value | | install.sh: no colon arm | 4 | the three colon values, the `$HOME` default | | install.sh: colon arm before the drive block | 17 | #726's drive cases (the respelling never runs) | | install.sh: colon arm after the release lookup | 4 | a request was made, for the colon values | | install.sh: colon refusal names nothing | 4 | the three colon values, the `$HOME` default | All 18 are killed, on the final head. A first run on the pre-reorder tests gave the same counts. - origin/main bf1f099: clean. The parent a3cde99: clean (this head descends from it). - All 47 `refs/heads/fix/*` and `l3-*` branches on origin (none of `l2-*` or `l4-*` exists; mine excluded): clean. Only #726's branch edits `install.sh`, `openRepoShape`, `tests/test_install_sh.py` or `tests/test_openreposhape_command.py`, as the issue said. - Lane 5's other wave-7 branches (`fix/757` through `fix/777`, local heads at the time): clean; none edits these four files. - **bash 3.2** is not here; lane 1 measured the table and the fix on 3.2.57 built from GNU source. - **Windows.** A control character, or a non-ASCII value, crossing the environment into Git Bash is not run here: the letter and U+009B tests skip on Windows, and the link and `$HOME` tests do too (no file there holds a control character or a `:`). The 11-value test runs there as #726's newline case did, and its result is `tests-git-bash`'s to give. CRLF `write_text` was emulated (244 passed). - **macOS** and a locale other than `C`, `POSIX` and `C.UTF-8`: not run. No test skips for want of a locale. - **A GBK or GB18030 locale.** The bytes `0xC2 0x80` to `0xC2 0x9F` are 32 hanzi there, and the arm now refuses a directory that holds one as "a control character". Measured with Python's codecs only: no such locale is installed here. - **Release notes.** `--install` and install.sh now refuse a control character in `$OPENREPOSHAPE_BIN_DIR` where `--install` placed the command and printed the value as typed, and install.sh refuses a `:` before its fetch. Lane 3's #630 drafts `docs/release-notes/v1.1.0.md`; a line for it: "`install.sh` and `openRepoShape --install` refuse a `$OPENREPOSHAPE_BIN_DIR` holding a control character (a newline, a carriage return, a tab, an escape or a C1 control), and `install.sh` a `:`, with exit 2 before it makes a request." Not edited here. - README, the handbook, `AGENTS.md`, `docs/cli.md`: no sentence became false. 1. **U+202E, U+200B, U+2028 and U+2029 in `$OPENREPOSHAPE_BIN_DIR` are echoed as typed** (the body's candidate 1, now also the two separators). None is a control a terminal acts on; U+202E can make a directory read as another. 2. **A lone 0x9B byte** (an 8-bit C1 control, not UTF-8) matches in no locale, before and after this commit. Only an 8-bit terminal with C1 controls on acts on it. (Lane 1's candidate 2.) 3. **A GBK or GB18030 locale reads `0xC2 0x80` to `0xC2 0x9F` as hanzi**, so the arm refuses a directory holding one. Measured with codecs only. 4. **Lane 1's other candidates stand as they listed them:** a leading `-` makes `--install` run `mkdir -p "$dir"` with no `--`; U+202E and U+200B; a `$HOME` holding a control character is refused without saying the directory came from `$HOME`; install.sh's Windows-spelling refusal names nothing where the command's names `'$dir'`; macOS's `[[:cntrl:]]` for C1 under `en_US.UTF-8` is unmeasured. Review: lane 1's re-verification of #804 is PR-READY (#804 issuecomment-6238973516). CI (Brett Heap's ruling 27): the head 7e4181c passed tests, tests-git-bash and SonarCloud; main has touched none of its files since 6626a3c; and the local gate passed on its predicted squash tree cbc85da over main a9d1579: the install_sh, openreposhape_command, repo_hygiene, agents_md_codes and cli_reference sets, 404 passed. Landed on Brett Heap's word of 2026-10-17T21:47:28Z, in lane 5's session: "merge lane 5's v1.1.1 PRs once reviewed and green". Closes #760 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>



Lane: openreposhape-5 (openRepoShape-5)
Claim: #667 (comment)
#649's two leftovers (lane 5's checklist lines on #461). One commit, a3cde99, on main 35133f2 (#656 is in it). It changes
install.sh,openRepoShapeand the two suitestests-git-bashruns,tests/test_install_sh.pyandtests/test_openreposhape_command.py, and nothing else.git show --numstat:install.sh+32/-4,openRepoShape+57/-1,test_install_sh.py+155/-2,test_openreposhape_command.py+216/-13. README, the handbook anddocs/cli.mdare unchanged: every sentence about--install, the PATH note and$OPENREPOSHAPE_BIN_DIRstays true (export PATH=…is still what they say).What changed and why
1. install.sh applies #656's rule where it reads the variable (
main, before the symlink check and the fetch).target="$bin_dir/openRepoShape"was built from the raw value. WithD:\tools\bin, thepreflight:line and the fourdone:lines (andinstall:for an old release) saidD:\tools\bin/openRepoShape, while the command's own lines said/d/tools/bin/openRepoShape. With nocygpath, the command's--installrefused (exit 2) after install.sh had fetched it, and install.sh said only "the openRepoShape's --install refused (exit 2)".[A-Za-z]:/or[A-Za-z]:\is spelled withcygpath -u --, so every line names one directory. Where nocygpathanswers, fails, or answers a drive spelling again, install.sh refuses with exit 2 before the symlink check and before any request. Thecaseis a copy ofinstall_commands's, because install.sh is downloaded alone.2. The PATH note quotes the directory it prints (
export_path_line, new, inopenRepoShape).export PATH="$dir:$PATH", the directory as it was between double quotes. Fora"b$HOME cit printedexport PATH="…/a"b$HOME c:$PATH", whichevalrejects (unexpected EOF while looking for matching"). Foraecho X`` it printed a line that RUNSecho Xwhen pasted, and for `a$b` one that drops `$b`.",$, backquote,\or!, the line isexport PATH='<dir>':"$PATH", with'\''for an apostrophe. Every other directory keeps today's line byte for byte, a space or an apostrophe included (both are safe in double quotes).!is in the set because an interactive bash reads it inside double quotes as history:echo "a!b"answersbash: !b: event not foundat a prompt, and a person pastes this line at a prompt. A profile file never does.say " $(export_path_line "$dir")", in place of the old inline line.3.
--installrefuses a directory holding:or a newline (install_commands, beside #656's early checks), before anything is placed, with exit 2 and the remedy.:splits a$PATHwhatever the quoting, and a newline breaks the printed line in two, so no line about$PATHcan name them.:by definition, is respelled without one, and refuses what it cannot respell. Acygpathanswer that holds a:is refused there too.$HOME/.local/bin; the colon refusal names it.Decisions and their evidence
machine:row findspython, a drive-letter$OPENREPOSHAPE_REPOis a path, a Windows-spelled$OPENREPOSHAPE_BIN_DIRis respelled or refused, andtests-git-bashruns #598's installer cases (#649) #656's decision for the command, and install.sh must not answer one value differently: a test feeds each value through both (below).:and newline refusals are the command's (issue line 2). Where install.sh is handed such a value, the command refuses after the fetch, as it always did. Named below as a checklist candidate.export PATH='<dir>':"$PATH". The directory is the only quoted part. The probea"b$HOME cnow printsexport PATH='…/a"b$HOME c':"$PATH", andevalof it sets$PATHto exactly the directory plus the old$PATH.a\bsurvives double quotes (\bis not special there). The issue lists\, and one rule is easier to hold than "a backslash before one of four characters".${dir//…}. How a pattern substitution reads a quote in its replacement has changed between bash releases, this runs on 3.2, andsedis not one of the programs--installuses (INSTALL_NEEDS). I could not run bash 3.2 here (5.2.21 only); the loop uses${x%%"$q"*}and${x#*"$q"}, which are long-standing 3.2 forms.BIN_DIR_SPELLINGSgainedlower-case(d:\tools\bin). openRepoShape in Git Bash: themachine:row findspython, a drive-letter$OPENREPOSHAPE_REPOis a path, a Windows-spelled$OPENREPOSHAPE_BIN_DIRis respelled or refused, andtests-git-bashruns #598's installer cases (#649) #656's two tests now run over it too (+5 cases, 15 in all, all passing on both the base and the head), so[A-Za-z]is pinned in the command as well as in the copy.Tests (Linux, bash 5.2.21; every run through the lane's
run9.sh: gh stub first, bogus token, emptyGH_CONFIG_DIR)New: 56 cases. In
tests/test_openreposhape_command.py(39 with #656's two extended): the PATH note's lines for 12 names,export_path_linealone for the same 12 (so a"and a\are held on Windows, where no file can be named with one), 3 throughbash -i, 5 refusals (a colon, two directories, a drive-relative name, a newline, a drive spelling holding a newline), the$HOMEdefault, acygpathanswer holding a colon, and +5 from thelower-casespelling. Intests/test_install_sh.py(17): one value through install.sh and the command, in 3 spellings × 4cygpathstates (12; where it spells, both place the command at the same directory and name no other; where it does not, both refuse with exit 2 and install.sh made no request); everypreflight:,install:anddone:line with the directory respelled, over the four endings (4); and the symlink check seeing the respelled directory (1).47 failed, 19 passedin the new selection. The 19 are openRepoShape in Git Bash: themachine:row findspython, a drive-letter$OPENREPOSHAPE_REPOis a path, a Windows-spelled$OPENREPOSHAPE_BIN_DIRis respelled or refused, andtests-git-bashruns #598's installer cases (#649) #656's own cases and the plain, space and apostrophe names, which keep today's line.tests/test_openreposhape_command.py tests/test_install_sh.py tests/test_install_sh_filter_repo.py tests/test_openreposhape_package.py: 276 passed in 589.67s (the machine's load average was 115).tests/test_repo_hygiene.py tests/test_workflow_concurrency.py tests/test_release_workflow.py tests/test_readme_sections.py tests/test_cli_reference.py tests/test_setup_by_hand_commands.py tests/test_readme_windows_quoting_sentence.py tests/test_gh_guard.py: 250 passed in 175.17s.tests-git-bashruns, with Windows'write_textemulated (a plugin makes everyPath.write_textwrite CRLF): 218 passed in 326.60s. A probe test takes the plugin's effect: 1 passed with it, 1 failed without.actionlint: not run, no workflow is touched (tests.ymlis as on main).Mutation probes
Each row reverts one arm, refusal or hunk in a throwaway clone and runs the new cases and #656's (66 selected). The first column is how many failed.
backslashandlower-case, line cases, symlinkslash[A-Z]for[A-Za-z]lower-casecygpathcallspells(3), line cases, symlinkspellsagaincygpath -uwithout--spells, line cases, symlinkspellsa-newline,a-drive-spelling-and-a-newlinea-drive-spelling-and-a-newline(the value would be echoed)$HOMEdefault, respelled answer"not quoteda-double-quote, both tests$not quoteda-dollar,an-apostrophe-and-a-dollar, both tests,bash -ia-backquote, both tests\not quoteda-backslash,a-trailing-backslash, both tests!not quoteda-bang,two-apostrophes-and-a-bang, both tests,bash -ian-apostrophe, both tests (today's line kept)not-on-pathcases (3)bash -iAll 25 mutations are killed. A first version of "an apostrophe alone forces single quotes" was a syntax error, so every case failed; it was rewritten with
\'and re-run, and the row is that run.Reproductions (local fakes only;
repro.py)On the base (35133f2 code) and on the head:
D:\tools\bin, acygpaththat spells it: base linespreflight: D:\tools\bin/openRepoShape --preflightanddone: … installed at D:\tools\bin/openRepoShape, but the preflight did not run to the end (exit 127)(the exit 127 is Linux: the drive spelling resolves only in Git Bash). Head:preflight: <dir>/openRepoShape --preflightanddone: … installed at <dir>/openRepoShape, pinned to the release tag, and this machine is ready, exit 0.cygpath: base exit 2 after 2 requests (the redirect and the fetch), with the command's refusal. Head: exit 2, 0 requests, install.sh's own refusal.a"b$HOME c→evalfails withunexpected EOF;a$b→$PATHgets…/a;a`echo X`→$PATHgets…/aX(the backquote ran). Head: all three give exactly the directory plus the old$PATH. A backslash, a bang, an apostrophe, a space and a plain name keep or gain the right line (the first two single-quoted, the last three today's line).:or a newline: base exit 0 and the command placed under it, with a lineevalcannot use. Head exit 2, nothing placed.cygpath -uon a real drive path, a realD:. Not faked as real; the PR'stests-git-bashrun checks it through the tests (below).Merge checks (
git merge-tree --write-tree, after a fresh fetch; refs deleted afterwards)+and an f-string's pieces, and flags a#led by two or more blanks or a tab with or without a blank after it and a#that opens text after a part that is not a constant, socmd #x,cmd + " " + "# x"andf"{cmd}{pad}# x"no longer reach a NEXT unseen (#679) #698,tests/test_doctor_next_is_a_command.pyonly): clean.pipx install openreposhapeis the second way in and the first on native Windows, git-filter-repo is installed in install.sh's order and never with a barepip install, and a first draft of the v1.1.0 release notes (#563) #630l3-563-docs5b140bd: conflicts indocs/handbook.html, and the same single file conflicts between README, AGENTS.md, docs/cli.md and the handbook:pipx install openreposhapeis the second way in and the first on native Windows, git-filter-repo is installed in install.sh's order and never with a barepip install, and a first draft of the v1.1.0 release notes (#563) #630 and main de92614 alone (README, AGENTS.md, docs/cli.md and the handbook:pipx install openreposhapeis the second way in and the first on native Windows, git-filter-repo is installed in install.sh's order and never with a barepip install, and a first draft of the v1.1.0 release notes (#563) #630 is lane 3's to refresh). README, AGENTS.md, docs/cli.md and the handbook:pipx install openreposhapeis the second way in and the first on native Windows, git-filter-repo is installed in install.sh's order and never with a barepip install, and a first draft of the v1.1.0 release notes (#563) #630 touches none of this PR's four files, so my change adds no region.l3-668,l3-669,l3-677,l3-678; lane 4'sfix/610,fix/645,fix/646,fix/648,fix/650,fix/651,fix/653,fix/655,fix/658,fix/659,fix/664,fix/696: clean, and none touchesinstall.sh,openRepoShapeor the five installer and launcher suites.The Windows run (what I could not check)
write_text(218 passed). No new assert compares a path bash printed with a Python path (the PATH-note line is built frombash_path(tmp_path), as openRepoShape in Git Bash: themachine:row findspython, a drive-letter$OPENREPOSHAPE_REPOis a path, a Windows-spelled$OPENREPOSHAPE_BIN_DIRis respelled or refused, andtests-git-bashruns #598's installer cases (#649) #656's tests are).windows-latest, Git Bash's realcygpathfor the new install.sh cases (3 spellings, throughput_cygpath, which puts a#!/bin/shwrapper aroundshutil.which("cygpath")on that$PATH, asput_on_pathdoes formktemp); the 12 PATH-note names that can be created there (a$b, a backquote,!, a space, an apostrophe); thebash -sruns that carry the script as bytes and the directory in the environment; and a newline or a:in$OPENREPOSHAPE_BIN_DIRcrossing the environment into Git Bash. The names with"or\, thebash -icases, the$HOMEdefault and the respelled-colon case skip on Windows;export_path_lineis still run for the"and\names there, by environment, not by a file name. If one fails there, its log shows the typed value and the printed line./bin/bash) was not available: only the loop's forms are claimed for it.Left out
:and newline refusals. See candidate 1.--installnow refuses a:or newline in the directory, where it used to place the command and print an unusable line: a visible change fordocs/release-notes/v1.1.0.md(lane 3's README, AGENTS.md, docs/cli.md and the handbook:pipx install openreposhapeis the second way in and the first on native Windows, git-filter-repo is installed in install.sh's order and never with a barepip install, and a first draft of the v1.1.0 release notes (#563) #630). Not edited here.docs/cli.md: no sentence became false.Checklist candidates (not filed)
:or a newline in$OPENREPOSHAPE_BIN_DIRonly after its fetch. Handed such a value, install.sh fetches the launcher, and the command refuses with exit 2; install.sh says "the openRepoShape's --install refused (exit 2); its own refusal above says why". A release older than this change does not refuse at all. Copying the twocasearms intomain, before the symlink check, would make install.sh refuse first and name the variable, as it now does for the drive spelling.is not on $PATHline; only the newline is refused first. It cannot forge a line, but an escape sequence reaches the terminal.Closes #667
🤖 Generated with Claude Code
Summary by Sourcery
Harden installation across Windows-style directories and unsafe PATH values while ensuring generated PATH instructions remain safe to use.
Bug Fixes:
$OPENREPOSHAPE_BIN_DIRvalues consistently during installation and refuse them safely when they cannot be converted.Enhancements:
Tests: