Skip to content

Commit 2e9e62d

Browse files
UnbreakableMJclaude
andcommitted
fix(spacecraft-cli-shell): add missing powershell.md + ash.md references
SKILL.md Step 3 routed Brush, PowerShell, and ash targets to reference files, but references/powershell.md and references/ash.md never existed — a dangling-reference gap predating the Brush work. PowerShell and ash targets silently fell through to posix-safe.md/bashisms.md with no shell-specific guidance. - references/powershell.md: full divergent-shell reference (object pipeline, -eq/-lt word operators, param() functions, splatting, stream redirection, try/catch, gotchas, bash->PowerShell translation table) — same depth as ion.md/nushell.md. - references/ash.md: concise differences-from-POSIX file (BusyBox/dash family) pointing back to posix-safe.md, with ash-specific quirks (BusyBox echo, applet flag gaps, /bin/sh-is-ash on Alpine) and the bash features ash lacks. Both headerless to match the existing references (repo REUSE.toml ** default). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 0a33a82 commit 2e9e62d

4 files changed

Lines changed: 302 additions & 0 deletions

File tree

spacecraft-cli-shell.skill

5.7 KB
Binary file not shown.

spacecraft-cli-shell.zip

5.7 KB
Binary file not shown.
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# ash — Almquist Shell (BusyBox / dash family)
2+
3+
ash is a small POSIX shell. You meet it as **BusyBox `ash`** (Alpine Linux,
4+
Docker base images, embedded/initramfs) and, closely related, as **`dash`**
5+
(Debian/Ubuntu `/bin/sh`). The skill detects it from `#!/bin/ash`; on Alpine,
6+
`/bin/sh` *is* ash, so a `#!/bin/sh` script there runs under ash too.
7+
8+
> **ash is POSIX — so `references/posix-safe.md` is the real reference.** Write
9+
> POSIX sh and it runs unchanged. This file records only the ash-specific
10+
> quirks and the bash features ash lacks (which bite when someone drops a
11+
> "`/bin/sh`" script that was secretly relying on bash).
12+
13+
Docs: BusyBox ash (<https://busybox.net/downloads/BusyBox.html#ash>), dash
14+
manual (<https://manpages.debian.org/dash>).
15+
16+
## What ash gives you beyond strict POSIX
17+
18+
- **`local` in functions** — both BusyBox ash and dash support `local var`,
19+
so you can scope variables. (Strict POSIX `sh` does not guarantee it, but for
20+
an ash target it's safe.)
21+
- **`test` / `[ ]`, `case`, `$(( ))`, `$( )`, heredocs, `trap`** — all present
22+
and behave as POSIX specifies.
23+
- **`set -e`, `set -u`** — supported. `set -o pipefail` is **not** in dash and
24+
is only in newer BusyBox ash builds — do not rely on it; check exit status
25+
explicitly instead.
26+
27+
## What ash does NOT have (the bash trap)
28+
29+
These parse in bash but fail — often silently — under ash. If a script needs
30+
one, either rewrite in POSIX or change the target to Bash:
31+
32+
- **`[[ … ]]`** — use `[ … ]` / `test`.
33+
- **Arrays** (`arr=(a b c)`, `${arr[@]}`) — none. Use positional params, a
34+
space-separated string with `set --`, or iterate with `for`.
35+
- **`${var^^}` / `${var,,}` / `${var//a/b}`** — use `tr` / `sed`.
36+
- **Brace expansion** `{1..10}` and `{a,b,c}` — use `seq` or enumerate.
37+
- **`function name { … }`** keyword form — use `name() { … }`.
38+
- **`<<<` here-strings** and **`<(…)` process substitution** — use a pipe,
39+
`printf … |`, or a tempfile.
40+
- **`**` globstar** — use `find`.
41+
- **`$RANDOM` / `$SECONDS` / `$PIPESTATUS`** — not in dash; presence in BusyBox
42+
ash depends on build options. Use `awk`/`date +%s`/explicit status checks.
43+
44+
## BusyBox-specific gotchas
45+
46+
- **`echo` is unreliable** — BusyBox `echo` interprets backslash escapes and
47+
its `-e`/`-n` handling differs from GNU. Always use `printf '%s\n' "$x"` for
48+
anything but the most trivial literal output.
49+
- **Applets, not coreutils**`sed`, `grep`, `find`, `awk`, etc. are BusyBox
50+
reimplementations with a reduced flag set. A GNU-only flag (`grep -P`,
51+
`sed -i` with a backup suffix, `find -printf`) may be missing. Tool choice is
52+
`spacecraft-cli-preference`'s job; here, just prefer POSIX-defined options.
53+
- **`/bin/sh` symlink** — on Alpine it points at BusyBox ash, not bash. A
54+
`#!/bin/sh` script authored against bash will break on Alpine. This is the
55+
single most common "works on my Ubuntu, fails in the Alpine container" cause.
56+
57+
## Shebang
58+
59+
```sh
60+
#!/bin/sh
61+
set -eu
62+
```
63+
64+
Use `#!/bin/sh` for portability (runs under ash, dash, bash-in-POSIX-mode
65+
alike). Use `#!/bin/ash` only when you specifically need BusyBox ash and know
66+
it is installed at that path.
67+
68+
## Verify
69+
70+
`shellcheck -s sh script.sh` catches the bashisms above. For the strongest
71+
signal, run the script under `dash` (or in a `busybox sh`) — both reject
72+
bashisms that `bash` silently tolerates.
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
# PowerShell Syntax Reference
2+
3+
PowerShell is **object-oriented**, not text-oriented: pipelines pass .NET
4+
objects, not lines of text. It is not POSIX and does not accept Bash syntax.
5+
Target it for `.ps1` / `.psm1` / `.psd1` files, Windows-first tooling, and
6+
cross-platform automation where `pwsh` (PowerShell 7+) is the runtime. Prefer
7+
`pwsh` (cross-platform, MIT-licensed) over Windows PowerShell 5.1 for anything
8+
new.
9+
10+
Docs: <https://learn.microsoft.com/powershell/scripting/>
11+
12+
## Variables
13+
14+
```powershell
15+
$name = "mohamed" # spaces around = are fine and conventional
16+
[int]$n = 42 # typed
17+
[string[]]$files = 'a.rs','b.rs'
18+
$env:PATH = "$env:PATH:/extra" # environment variables live under $env:
19+
Remove-Variable name # unset
20+
```
21+
22+
- Sigil `$` is part of the name (`$name`), not just expansion — assignment uses
23+
it too: `$x = 1`, never `x=1`.
24+
- **No word-splitting.** `$files` stays one array; `"$name"` stays one string.
25+
This is the biggest mental shift from POSIX — you almost never need defensive
26+
quoting against splitting.
27+
- Environment variables are a drive: `$env:HOME`, `$env:USERPROFILE`.
28+
- `$null`, `$true`, `$false` are the literals.
29+
30+
## Everything is an object
31+
32+
```powershell
33+
Get-ChildItem *.rs | Where-Object Length -gt 1kb | Sort-Object Name
34+
Get-Process | Select-Object Name, Id | Format-Table
35+
```
36+
37+
- Cmdlets are `Verb-Noun` (`Get-ChildItem`, `Set-Content`, `Where-Object`).
38+
- The pipeline carries objects; `$_` (or `$PSItem`) is the current object inside
39+
`ForEach-Object` / `Where-Object`.
40+
- Reach for cmdlets over external binaries when a native one exists — you keep
41+
the objects instead of re-parsing text.
42+
43+
## Comparison operators (NOT `[[ ]]` / `-eq` is a word)
44+
45+
```powershell
46+
if ($n -eq 5) { } # equal
47+
if ($n -ne 5) { } # not equal
48+
if ($n -lt 5) { } # less than (-le -gt -ge similarly)
49+
if ($s -like '*.rs') { } # wildcard match
50+
if ($s -match '\.rs$'){ } # regex match, sets $matches
51+
if ($arr -contains 1){ } # membership
52+
```
53+
54+
- **No `<`, `>`, `==` for comparison** — those are redirection/assignment-ish.
55+
Use the dashed word operators.
56+
- Case-insensitive by default; prefix `c` for case-sensitive (`-ceq`, `-cmatch`)
57+
or `i` to force insensitive (`-ieq`).
58+
- Against an array, comparison operators **filter**: `1,2,3 -gt 1` returns
59+
`2,3`, not a boolean. Wrap in `@(...)`/check `.Count` when you mean a test.
60+
61+
## Conditionals
62+
63+
```powershell
64+
if ($x -gt 10) {
65+
"big"
66+
} elseif ($x -eq 10) {
67+
"ten"
68+
} else {
69+
"small"
70+
}
71+
72+
switch ($name) {
73+
'a.rs' { 'rust' }
74+
{ $_ -like '*.toml' } { 'config' }
75+
default { 'other' }
76+
}
77+
```
78+
79+
## Loops
80+
81+
```powershell
82+
foreach ($f in $files) { $f }
83+
for ($i = 0; $i -lt 10; $i++) { $i }
84+
while ($x -lt 100) { $x++ }
85+
do { $x++ } while ($x -lt 100)
86+
87+
Get-ChildItem *.rs | ForEach-Object { $_.Name } # pipeline form
88+
1..10 | ForEach-Object { $_ } # range operator
89+
```
90+
91+
- `1..10` is an inclusive range.
92+
- `foreach` (statement) and `ForEach-Object` (pipeline cmdlet) are different —
93+
the statement is faster for in-memory collections; the cmdlet streams.
94+
95+
## Functions
96+
97+
```powershell
98+
function Get-Greeting {
99+
param(
100+
[Parameter(Mandatory)][string]$Name,
101+
[int]$Times = 1
102+
)
103+
1..$Times | ForEach-Object { "hello $Name" }
104+
}
105+
106+
Get-Greeting -Name mohamed -Times 2
107+
```
108+
109+
- Name functions `Verb-Noun` with an approved verb (`Get`, `Set`, `New`,
110+
`Remove`, …; see `Get-Verb`).
111+
- Parameters go in a `param( )` block, typed and attributed. Add
112+
`[CmdletBinding()]` above `param` for an advanced function (gets `-Verbose`,
113+
`-ErrorAction`, etc. for free).
114+
- Output is whatever expression values fall out — no `return` needed (and
115+
`return $x` just emits `$x` then exits).
116+
117+
## Arrays, hashtables, splatting
118+
119+
```powershell
120+
$arr = 1, 2, 3 # or @(1,2,3)
121+
$arr += 4 # arrays are immutable; this rebuilds
122+
$arr.Count
123+
$map = @{ name = 'mj'; n = 42 }
124+
$map['name']; $map.name
125+
126+
$params = @{ Path = 'x.txt'; Encoding = 'utf8' }
127+
Set-Content @params # splatting: @params, not $params
128+
```
129+
130+
## Strings
131+
132+
```powershell
133+
"interpolated $name and $($n + 1)" # double quotes expand; $() for expressions
134+
'literal $name' # single quotes do NOT expand
135+
@"
136+
multi-line $name
137+
"@ # expanding here-string
138+
@'
139+
multi-line literal $name
140+
'@ # literal here-string
141+
```
142+
143+
- Sub-expression `$( … )` is required to interpolate anything beyond a bare
144+
variable (property access, arithmetic, method calls).
145+
146+
## Command substitution / external commands
147+
148+
```powershell
149+
$branch = (git rev-parse --abbrev-ref HEAD) # capture stdout as string(s)
150+
$count = (Get-Content notes.txt | Measure-Object -Line).Lines
151+
& $exe --flag # call operator for a path/var
152+
git status; $LASTEXITCODE # exit code of last native exe
153+
$? # success of last operation (bool)
154+
```
155+
156+
- `$(...)` is a sub-expression; `( )` captures pipeline output. Native command
157+
stdout comes back as an array of strings (split on newlines).
158+
- Use `&` (call operator) to run a command whose name is in a variable or has a
159+
space in its path.
160+
- Native-exe success is `$LASTEXITCODE`; cmdlet success is `$?`. They are not
161+
the same thing.
162+
163+
## Redirection
164+
165+
```powershell
166+
cmd > out.txt # success stream (1) to file
167+
cmd >> out.txt # append
168+
cmd 2> err.txt # error stream
169+
cmd *> all.txt # all streams
170+
cmd 2>&1 # merge error into success
171+
cmd | Out-File -Encoding utf8 out.txt
172+
$null = cmd # discard output (faster than | Out-Null)
173+
```
174+
175+
PowerShell has numbered streams beyond 1/2: `3` warning, `4` verbose, `5` debug,
176+
`6` information. `*>` captures them all.
177+
178+
## Error handling
179+
180+
```powershell
181+
$ErrorActionPreference = 'Stop' # make errors terminating (script-wide)
182+
183+
try {
184+
Get-Content missing.txt -ErrorAction Stop
185+
} catch {
186+
Write-Error "failed: $($_.Exception.Message)"
187+
} finally {
188+
# cleanup
189+
}
190+
191+
throw "fatal"
192+
```
193+
194+
- Many cmdlet errors are **non-terminating** by default and won't trigger
195+
`catch` unless you set `-ErrorAction Stop` (or `$ErrorActionPreference`).
196+
- The current error in `catch` is `$_`; the error collection is `$Error`.
197+
198+
## Gotchas
199+
200+
- **Not Bash.** `[[ ]]`, `$(( ))`, `${var^^}`, `for ((;;))`, `arr=(...)`, `&&`
201+
in older Windows PowerShell — none are PowerShell. (`&&`/`||` *do* work in
202+
pwsh 7+ as pipeline-chain operators, but not in 5.1.)
203+
- **Output is objects.** Piping to a text tool (`findstr`, `grep`) forces a
204+
string conversion and loses structure — prefer `Where-Object`/`Select-Object`.
205+
- **Execution policy** (Windows): scripts may be blocked. `pwsh -File x.ps1` or
206+
`Set-ExecutionPolicy -Scope Process RemoteSigned` for a one-off.
207+
- **Paths:** use `/` for cross-platform; `\` is Windows-only and is also the
208+
escape-ish char in some contexts. Prefer `Join-Path`.
209+
- **Case-insensitive** by default for comparisons, variable names, and cmdlet
210+
names — do not rely on case to disambiguate.
211+
- Pin the runtime: target `pwsh` 7+ for cross-platform scripts; note when a
212+
script requires Windows PowerShell 5.1 (e.g. uses a Windows-only module).
213+
214+
## Quick translation targets
215+
216+
| Bash / POSIX | PowerShell |
217+
|--------------|------------|
218+
| `foo=bar` | `$foo = 'bar'` |
219+
| `export FOO=bar` | `$env:FOO = 'bar'` |
220+
| `$(cmd)` | `(cmd)` or `$(cmd)` |
221+
| `if [ "$a" = "$b" ]` | `if ($a -eq $b)` |
222+
| `if [ -f x ]` | `if (Test-Path x)` |
223+
| `for f in *.rs; do …; done` | `foreach ($f in Get-ChildItem *.rs) { … }` |
224+
| `cmd > out 2>&1` | `cmd *> out` |
225+
| `grep pat file` | `Select-String pat file` |
226+
| `cat file` | `Get-Content file` |
227+
| `arr=(a b c); "${arr[0]}"` | `$arr = 'a','b','c'; $arr[0]` |
228+
| `${#arr[@]}` | `$arr.Count` |
229+
| `function foo { … }` | `function Foo { … }` |
230+
| `set -e` | `$ErrorActionPreference = 'Stop'` |

0 commit comments

Comments
 (0)