When working on projects related to shell scripting and wsl patterns, apply this domain knowledge.
Shell Scripting & WSL — Domain Knowledge
Shell Script Bug Patterns
exit vs return
return only works in sourced scripts or functions.
- When a script is executed (
./script.sh or bash script.sh), return is invalid.
- Use
exit 1 for executable scripts, return 1 inside functions.
return -1 is technically undefined behavior — use exit 1 or return 1.
Variable Quoting (CRITICAL)
- Always quote variables:
"$var", "$@", "$file".
- Unquoted variables cause word splitting on spaces/newlines.
$@ → "$@" — preserves argument boundaries.
rm /path/$var/* → rm "/path/$var/"* — prevents glob expansion of empty var.
Directory Safety
- Always
mkdir -p before writing to directories that may not exist.
rm dir/* fails if directory is empty — use rm -f dir/* 2>/dev/null || true
or check first: [ -d dir ] && find dir -type f -delete.
Error Handling
set -euo pipefail at the top of scripts:
-e: exit on error
-u: treat unset variables as errors
-o pipefail: pipe fails if any command fails (not just the last)
- Chain with
&& when you want dependent commands to stop on failure.
Upgrade / Update Script Patterns
git_pull_and_build Helper
git_pull_and_build() {
local repo_dir="$1"; shift
local old_head new_head
cd "$repo_dir"
old_head=$(git rev-parse HEAD)
git pull --ff-only
new_head=$(git rev-parse HEAD)
if [ "$old_head" = "$new_head" ]; then
echo "No changes, skipping rebuild"
return 0
fi
"$@" # Run build commands passed as remaining args
}
- Only rebuilds when
git pull brings new commits.
- Build commands passed as trailing arguments for flexibility.
Dependency Ordering
- Build in dependency order: e.g., ncurses → tmux, nano (both depend on ncurses).
- Each section is independent — failures propagate via
set -e.
Package Manager Detection
# Only run if the command exists
command -v snap >/dev/null 2>&1 && snap refresh
command -v npm >/dev/null 2>&1 && sudo npm update -g
command -v pip3 >/dev/null 2>&1 && pip3 install --user --upgrade <packages>
command -v rustup >/dev/null 2>&1 && rustup update
command -v cargo >/dev/null 2>&1 && cargo install-update --all
- Guard each section with
command -v — silently skipped if not installed.
npm update -g needs sudo when global prefix is root-owned (/usr/local).
Quieting Verbose Output (keep errors + status lines)
- Prefer
apt-get -qq over apt in scripts — apt-get is the stable scripting
interface and avoids the WARNING: apt does not have a stable CLI message; -qq silences
progress while still printing errors to stderr.
snap refresh >/dev/null (and similar) to drop chatty stdout; don't redirect stderr —
you want failures to surface.
- Keep the script's own headers/status
echos; only suppress the noisy stdout of the tools
it calls, so a run still reads as a clear progress log.
Systemd Detection
# Check for systemd (important for WSL where it may not be PID 1)
if [ -d /run/systemd/system ]; then
sudo fwupdmgr refresh && sudo fwupdmgr update
fi
fwupdmgr needs sudo to bypass polkit (unavailable without systemd).
- Snap requires systemd — won't function in WSL without it.
WSL-Specific Quirks
Systemd in WSL
- By default, WSL2 does NOT run systemd as PID 1.
- To enable: add to
/etc/wsl.conf:[boot]
systemd=true
- Then restart:
wsl --shutdown from PowerShell.
- Without systemd: snap, polkit, fwupd, and other systemd-dependent tools fail.
Terminal / Progress Indicators
APT Troubleshooting
- Broken repo files: Check
/etc/apt/sources.list.d/ for wrong URLs
(e.g., Edge repo pointing at Chrome URL).
- Legacy keyrings:
/etc/apt/trusted.gpg is deprecated — migrate keys to
/etc/apt/trusted.gpg.d/ as individual .gpg files.
- Stale local repos: Check
/var/cuda-repo-* and similar — can waste gigabytes.
Remove the .list file and the local repo directory.
apt-key is deprecated — use signed-by= in repo definitions.
Cross-Compilation from WSL
- Rsync sources to WSL native filesystem for better build performance
(avoid Windows filesystem overhead via
/mnt/c/).
- Visual Studio remote development presets work with WSL via CMake vendor settings.
Embedded Device Shell Patterns (mFi/OpenWrt)
Symlink-Based Config
- DRY principle: shared files (profile, rc.poststart, mqtt.ini) aren't duplicated.
- Device directories contain only symlinks to shared files + device-specific configs.
add.sh bootstraps a new device directory with appropriate symlinks.
Deployment Pipeline
# 1. Archive device config
tar czf /tmp/config.tar.gz -C device_dir .
# 2. SCP to device
scp /tmp/config.tar.gz ubnt@device.local:/tmp/
# 3. SSH: stop, deploy, commit, restart
ssh ubnt@device.local 'cd /var/etc/persistent && \\
/usr/bin/mfi-mqtt-client stop && \\
tar xzf /tmp/config.tar.gz -C bin/ && \\
cfgmtd -w -p /etc/ && \\
/var/etc/persistent/rc.poststart'
One-Connection Deploy (stream tar over SSH)
Collapse the separate scp + ssh into a single SSH connection by piping tar through it —
fewer auth round-trips, no temp file on the device:
tar -chf - -C "./$host/" ./ | ssh "ubnt@$host.local" \
'tar -xf - -C /var/etc/persistent/ && \
pkill -9 mfi-mqtt-client; \
cfgmtd -w -p /etc/ && /var/etc/persistent/rc.poststart'
-c = create, -h = follow symlinks (dereference the symlinked config into real files),
-f - = write archive to stdout; the remote tar -xf - reads it from stdin.
- Everything after the extract runs in the same remote shell, so stop/clean/commit/restart
need no extra connection.
Version-Aware Updater (compare against GitHub, no marker file)
- Rather than tracking installed version in a marker file, ask the installed tool
(
mytool --version → mytool 1.2.0) and compare against the latest GitHub release tag.
- Only download when the remote tag is newer. Handle the
<tool> <version> output and a
v-prefixed tag with the same portable semver parse/compare.
- A portable shell semver comparator (no
sort -V dependency assumed):ver_lt() { # returns 0 if $1 < $2
[ "$1" = "$2" ] && return 1
[ "$(printf '%s\n%s\n' "$1" "$2" | sort -t. -k1,1n -k2,2n -k3,3n | head -1)" = "$1" ]
}
Startup System
rc.poststart runs all executable scripts in rc.poststart.d/ in parallel (&).
- Modular: add new services by dropping scripts into the directory.
- Non-executable files are skipped.
Rust/Cargo Patterns (from WSL-Hello-sudo)
clippy -- -D warnings treats all warnings as errors (strict linting).
Cow<str> → Cow<'_, str> — always make elided lifetimes explicit.
#[allow(dead_code)] on enum variant fields that are matched structurally
but never read directly.
Some(code) if code == 0 → Some(0) — simplify redundant guards.
bindgen for C FFI bindings generation.
Cargo.lock Reproducibility (CRITICAL for binaries)
- Commit
Cargo.lock for binaries/applications (not for libraries). An unpinned
lockfile means every build re-resolves dependencies and picks the newest patch versions,
causing builds that "used to work" to break when a transitive dependency publishes an
incompatible patch.
- Example failure:
actix-web 4.13 → cookie 0.16.2 breaks when time >= 0.3.50 is resolved
(Parsable::parse signature changed), while simple_logger requires time >= 0.3.49 —
only time = 0.3.49 satisfied both. A fresh resolve picked 0.3.52 and broke the build.
- Fixes (in order of preference):
- Commit
Cargo.lock pinned to a working set (cargo generate-lockfile +
cargo update -p <crate> --precise <version>). Remove it from .gitignore.
- Constrain in
Cargo.toml: time = "=0.3.49" (works without a committed lock).
- Drop unused features pulling the problematic crate:
actix-web = { default-features = false, features = [...] } to exclude cookies.
- Docker gotcha: ensure the Dockerfile
COPYs the real Cargo.lock before
cargo build, otherwise the pin doesn't apply in-image.
- Diagnose transitive deps with
cargo tree -i <crate>.
1---2name: shell-wsl3description: Shell script bugs, WSL quirks, upgrade scripts, embedded device deployment, and Rust/Cargo4---5
6When working on projects related to shell scripting and wsl patterns, apply this domain knowledge.
7
8# Shell Scripting & WSL — Domain Knowledge
9
10## Shell Script Bug Patterns
11
12### exit vs return
13- `return` only works in **sourced** scripts or functions.
14- When a script is **executed** (`./script.sh` or `bash script.sh`), `return` is invalid.
15- Use `exit 1` for executable scripts, `return 1` inside functions.
16- `return -1` is technically undefined behavior — use `exit 1` or `return 1`.
17
18### Variable Quoting (CRITICAL)
19- Always quote variables: `"$var"`, `"$@"`, `"$file"`.
20- Unquoted variables cause word splitting on spaces/newlines.
21- `$@` → `"$@"` — preserves argument boundaries.
22- `rm /path/$var/*` → `rm "/path/$var/"*` — prevents glob expansion of empty var.
23
24### Directory Safety
25- Always `mkdir -p` before writing to directories that may not exist.
26- `rm dir/*` fails if directory is empty — use `rm -f dir/* 2>/dev/null || true`
27 or check first: `[ -d dir ] && find dir -type f -delete`.
28
29### Error Handling
30- `set -euo pipefail` at the top of scripts:
31 - `-e`: exit on error
32 - `-u`: treat unset variables as errors
33 - `-o pipefail`: pipe fails if any command fails (not just the last)
34- Chain with `&&` when you want dependent commands to stop on failure.
35
36## Upgrade / Update Script Patterns
37
38### git_pull_and_build Helper
39```bash
40git_pull_and_build() {
41 local repo_dir="$1"; shift
42 local old_head new_head
43 cd "$repo_dir"
44 old_head=$(git rev-parse HEAD)
45 git pull --ff-only
46 new_head=$(git rev-parse HEAD)
47 if [ "$old_head" = "$new_head" ]; then
48 echo "No changes, skipping rebuild"
49 return 0
50 fi
51 "$@" # Run build commands passed as remaining args
52}
53```
54- Only rebuilds when `git pull` brings new commits.
55- Build commands passed as trailing arguments for flexibility.
56
57### Dependency Ordering
58- Build in dependency order: e.g., ncurses → tmux, nano (both depend on ncurses).
59- Each section is independent — failures propagate via `set -e`.
60
61### Package Manager Detection
62```bash
63# Only run if the command exists
64command -v snap >/dev/null 2>&1 && snap refresh
65command -v npm >/dev/null 2>&1 && sudo npm update -g
66command -v pip3 >/dev/null 2>&1 && pip3 install --user --upgrade <packages>
67command -v rustup >/dev/null 2>&1 && rustup update
68command -v cargo >/dev/null 2>&1 && cargo install-update --all
69```
70- Guard each section with `command -v` — silently skipped if not installed.
71- `npm update -g` needs `sudo` when global prefix is root-owned (`/usr/local`).
72
73### Quieting Verbose Output (keep errors + status lines)
74- Prefer **`apt-get -qq`** over `apt` in scripts — `apt-get` is the stable scripting
75 interface and avoids the `WARNING: apt does not have a stable CLI` message; `-qq` silences
76 progress while still printing errors to stderr.
77- `snap refresh >/dev/null` (and similar) to drop chatty stdout; **don't** redirect stderr —
78 you want failures to surface.
79- Keep the script's *own* headers/status `echo`s; only suppress the noisy stdout of the tools
80 it calls, so a run still reads as a clear progress log.
81
82### Systemd Detection
83```bash
84# Check for systemd (important for WSL where it may not be PID 1)
85if [ -d /run/systemd/system ]; then
86 sudo fwupdmgr refresh && sudo fwupdmgr update
87fi
88```
89- `fwupdmgr` needs `sudo` to bypass polkit (unavailable without systemd).
90- Snap requires systemd — won't function in WSL without it.
91
92## WSL-Specific Quirks
93
94### Systemd in WSL
95- By default, WSL2 does NOT run systemd as PID 1.
96- To enable: add to `/etc/wsl.conf`:
97 ```ini
98 [boot]
99 systemd=true
100 ```
101- Then restart: `wsl --shutdown` from PowerShell.
102- Without systemd: snap, polkit, fwupd, and other systemd-dependent tools fail.
103
104### Terminal / Progress Indicators
105- `TERM=xterm-color` is too limited — causes Copilot CLI to skip progress indicators.
106- Fix: set `TERM=xterm-256color` in tmux config:
107 ```
108 set -g default-terminal "xterm-256color"
109 ```
110- Then restart tmux (`tmux kill-server`).
111
112### APT Troubleshooting
113- **Broken repo files**: Check `/etc/apt/sources.list.d/` for wrong URLs
114 (e.g., Edge repo pointing at Chrome URL).
115- **Legacy keyrings**: `/etc/apt/trusted.gpg` is deprecated — migrate keys to
116 `/etc/apt/trusted.gpg.d/` as individual `.gpg` files.
117- **Stale local repos**: Check `/var/cuda-repo-*` and similar — can waste gigabytes.
118 Remove the `.list` file and the local repo directory.
119- `apt-key` is deprecated — use `signed-by=` in repo definitions.
120
121### Cross-Compilation from WSL
122- Rsync sources to WSL native filesystem for better build performance
123 (avoid Windows filesystem overhead via `/mnt/c/`).
124- Visual Studio remote development presets work with WSL via CMake vendor settings.
125
126## Embedded Device Shell Patterns (mFi/OpenWrt)
127
128### Symlink-Based Config
129- DRY principle: shared files (profile, rc.poststart, mqtt.ini) aren't duplicated.
130- Device directories contain only symlinks to shared files + device-specific configs.
131- `add.sh` bootstraps a new device directory with appropriate symlinks.
132
133### Deployment Pipeline
134```bash
135# 1. Archive device config
136tar czf /tmp/config.tar.gz -C device_dir .
137# 2. SCP to device
138scp /tmp/config.tar.gz ubnt@device.local:/tmp/
139# 3. SSH: stop, deploy, commit, restart
140ssh ubnt@device.local 'cd /var/etc/persistent && \\
141 /usr/bin/mfi-mqtt-client stop && \\
142 tar xzf /tmp/config.tar.gz -C bin/ && \\
143 cfgmtd -w -p /etc/ && \\
144 /var/etc/persistent/rc.poststart'
145```
146
147### One-Connection Deploy (stream tar over SSH)
148Collapse the separate `scp` + `ssh` into a **single** SSH connection by piping tar through it —
149fewer auth round-trips, no temp file on the device:
150```bash
151tar -chf - -C "./$host/" ./ | ssh "ubnt@$host.local" \
152 'tar -xf - -C /var/etc/persistent/ && \
153 pkill -9 mfi-mqtt-client; \
154 cfgmtd -w -p /etc/ && /var/etc/persistent/rc.poststart'
155```
156- `-c` = create, `-h` = **follow symlinks** (dereference the symlinked config into real files),
157 `-f -` = write archive to stdout; the remote `tar -xf -` reads it from stdin.
158- Everything after the extract runs in the *same* remote shell, so stop/clean/commit/restart
159 need no extra connection.
160
161### Version-Aware Updater (compare against GitHub, no marker file)
162- Rather than tracking installed version in a marker file, ask the installed tool
163 (`mytool --version` → `mytool 1.2.0`) and compare against the latest GitHub release tag.
164- Only download when the remote tag is newer. Handle the `<tool> <version>` output and a
165 `v`-prefixed tag with the same portable semver parse/compare.
166- A portable shell semver comparator (no `sort -V` dependency assumed):
167 ```bash
168 ver_lt() { # returns 0 if $1 < $2
169 [ "$1" = "$2" ] && return 1
170 [ "$(printf '%s\n%s\n' "$1" "$2" | sort -t. -k1,1n -k2,2n -k3,3n | head -1)" = "$1" ]
171 }
172 ```
173
174### Startup System
175- `rc.poststart` runs all executable scripts in `rc.poststart.d/` in parallel (`&`).
176- Modular: add new services by dropping scripts into the directory.
177- Non-executable files are skipped.
178
179## Rust/Cargo Patterns (from WSL-Hello-sudo)
180- `clippy -- -D warnings` treats all warnings as errors (strict linting).
181- `Cow<str>` → `Cow<'_, str>` — always make elided lifetimes explicit.
182- `#[allow(dead_code)]` on enum variant fields that are matched structurally
183 but never read directly.
184- `Some(code) if code == 0` → `Some(0)` — simplify redundant guards.
185- `bindgen` for C FFI bindings generation.
186
187### Cargo.lock Reproducibility (CRITICAL for binaries)
188- **Commit `Cargo.lock` for binaries/applications** (not for libraries). An unpinned
189 lockfile means every build re-resolves dependencies and picks the newest patch versions,
190 causing builds that "used to work" to break when a transitive dependency publishes an
191 incompatible patch.
192- Example failure: `actix-web 4.13 → cookie 0.16.2` breaks when `time >= 0.3.50` is resolved
193 (`Parsable::parse` signature changed), while `simple_logger` requires `time >= 0.3.49` —
194 only `time = 0.3.49` satisfied both. A fresh resolve picked 0.3.52 and broke the build.
195- **Fixes** (in order of preference):
196 1. Commit `Cargo.lock` pinned to a working set (`cargo generate-lockfile` +
197 `cargo update -p <crate> --precise <version>`). Remove it from `.gitignore`.
198 2. Constrain in `Cargo.toml`: `time = "=0.3.49"` (works without a committed lock).
199 3. Drop unused features pulling the problematic crate:
200 `actix-web = { default-features = false, features = [...] }` to exclude `cookies`.
201- **Docker gotcha**: ensure the Dockerfile `COPY`s the real `Cargo.lock` before
202 `cargo build`, otherwise the pin doesn't apply in-image.
203- Diagnose transitive deps with `cargo tree -i <crate>`.