POSIX shell scripting standards for Mark's workflow.
Scanned 9/2/2026
Install to Claude Code
npx -y skills add majiayu000/claude-skill-registry --skill shell-majo --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Shell Majo?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/majiayu000-shell-majo-claude-skill-registry)More formats (shields.io, HTML) on the badges page.
---
name: shell-majo
description: POSIX shell scripting standards for Mark's workflow.
license: Unlicense OR 0BSD
metadata:
author: Mark Joshwel <mark@joshwel.co>
version: "2026.2.2"
---
# POSIX Shell Scripting Standards (Mark)
**Goal**: Write portable, defensive shell scripts using pure POSIX sh (not bash) unless a specific Bash feature is strictly required.
## When to Use This Skill
- **Writing new shell scripts** (`.sh` files)
- **Modifying existing shell scripts**
- **Choosing between POSIX sh and Bash**
- **Adding error handling to scripts**
- **Creating command-line tools**
- **Writing automation scripts**
## When NOT to Use This Skill
- **Script requires Bash-specific features** (arrays, process substitution, extglob)
- **Target environment guarantees Bash availability** AND requires Bash features
- **Complex data structures needed** that can't be flattened to strings
- **Writing Python/JS scripts instead** (prefer those for complex logic)
## Process
1. **Check script requirements** - Determine if POSIX sh is sufficient or Bash is truly needed
2. **Set shebang** - Use `#!/bin/sh` for POSIX, `#!/bin/bash` only if enumerated exception applies
3. **Add license header** - Include full Unlicense header (see File Header section)
4. **Enable error handling** - Add `set -e` at the top
5. **Define exit codes** - Document exit code ranges in comments
6. **Use proper naming** - UPPER_CASE for env vars/constants, lower_case for local vars
7. **Quote all variables** - Prevent word splitting: `"$var"` not `$var`
8. **Use printf not echo** - For portable output
9. **Test with shellcheck** - Run `shellcheck script.sh`
10. **Update AGENTS.md** - Document any project-specific shell patterns
## Constraints
- **ALWAYS default to POSIX sh** for all scripts
- **ONLY use Bash if you can explicitly enumerate** which Bash-specific feature is strictly required and impossible in POSIX sh
- **Valid Bash exceptions**: arrays (indexed/associative), extglob patterns, process substitution, controlled Bash environments
- **"Cleaner" or "fewer lines" is NOT sufficient justification** for Bash
- **Scripts must be compatible with `/bin/sh`** on any POSIX-compliant system
- **Use `#!/bin/sh`** - Never use `#!/bin/bash` or `#!/usr/bin/env bash` without enumerated exception
Shell scripting standards following pure POSIX sh (not bash).
## Core Principle
**Default to POSIX sh for all scripts unless you can explicitly enumerate which Bash-specific feature is strictly required and impossible to replicate in POSIX sh.**
Treat complexity as a cost, not a feature. Choose Bash only when:
1. **You need arrays** (indexed or associative) for data structures that cannot be reasonably flattened to strings/positional params
2. **You require glob matching** with extglob patterns
3. **You need process substitution** for feeding command output as a file descriptor
4. **You're scripting exclusively for Bash environments** (NixOS activation scripts, Git hooks on controlled systems)
**If the justification is merely "it's cleaner" or "fewer lines," that is not sufficient** — POSIX sh is the correct choice.
Scripts must be compatible with `/bin/sh` on any POSIX-compliant system unless one of the above exceptions applies.
## Shebang
Always use:
```bash
#!/bin/sh
```
Never use `#!/bin/bash` or `#!/usr/bin/env bash`.
## File Header
All shell scripts must include the full Unlicense header:
```bash
#!/bin/sh
# script_name: brief description
# --------------------------------
# by mark <mark@joshwel.co>
#
# This is free and unencumbered software released into the public domain.
#
# Anyone is free to copy, modify, publish, use, compile, sell, or
# distribute this software, either in source code form or as a compiled
# binary, for any purpose, commercial or non-commercial, and by any
# means.
#
# In jurisdictions that recognize copyright laws, the author or authors
# of this software dedicate any and all copyright interest in the
# software to the public domain. We make this dedication for the benefit
# of the public at large and to the detriment of our heirs and
# successors. We intend this dedication to be an overt act of
# relinquishment in perpetuity of all present and future rights to this
# software under copyright law.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
#
# For more information, please refer to <http://unlicense.org/>
```
## Variables
### Naming
Use UPPER_CASE for environment variables and constants:
```bash
# defaults
SURPLUS_CMD_DEFAULT="surplus -td"
LOCATION_TIMEOUT=50
# environment overrides with defaults
SURPLUS_CMD="${SURPLUS_CMD:-$SURPLUS_CMD_DEFAULT}"
LOCATION_CMD="${LOCATION_CMD:-$LOCATION_CMD_DEFAULT}"
```
### Local Variables
Use lower_case for local/script variables:
```bash
status=0
bridge_failures=0
```
### Variable Expansion
Always quote variables to prevent word splitting:
```bash
# Good
mkdir -p "$SPOW_CACHE_DIR"
cat "$SPOW_NETLC_OUT" >>"$SPOW_SESH_OUT"
# Bad (unquoted)
mkdir -p $SPOW_CACHE_DIR
```
Use `${var:-default}` for defaults:
```bash
LOCATION_TIMEOUT="${LOCATION_TIMEOUT:-50}"
```
## Error Handling
### Message Format
Use the same format as `dev-standards-majo` (see that skill for full details):
```
program: level: message
```
**Examples:**
```bash
printf "s+ow: error: surplus is not installed\n" >&2
printf "s+ow: warn: using fallback location\n" >&2
printf "s+ow: info: starting location fetch\n" >&2
```
**Follow-up notes** for additional context:
```bash
printf "s+ow: error: location fetch failed\n" >&2
printf "... note: timeout was %d seconds\n" "$LOCATION_TIMEOUT" >&2
```
### Exit on Error
Use `set -e` at the top of scripts:
```bash
#!/bin/sh
set -e
```
### Command Checking
Check if commands exist before using:
```bash
if ! command -v "$SURPLUS_EXE" >/dev/null 2>&1; then
printf "s+ow: error: surplus is not installed\n" >&2
exit 2
fi
```
### Exit Codes
Use the grouping convention from `dev-standards-majo`:
| Range | Category |
|-------|----------|
| `0` | Success |
| `1-9` | Usage errors (bad args, missing env) |
| `10-19` | Input errors |
| `20-29` | File/IO errors |
| `255` | Runtime error |
**Example header:**
```bash
# exit codes:
# 0 - success
# 1 - bad command usage
# 2 - missing dependency
# 20 - file not found
```
## Control Structures
### Conditionals
Use POSIX test syntax:
```bash
if [ "$LOCATION_PRIORITISE_NETWORK" = "n" ]; then
LOCATION_PRIORITISE_NETWORK=""
fi
# Check if variable is set
if [ -n "$SPOW_PRIVATE" ]; then
SPOW_SESH_OUT="/dev/null"
fi
# Check if file exists and is non-empty
if [ -s "$SPOW_NETLC_OUT" ]; then
printf "net"
fi
```
### Loops
Use POSIX-compliant loops:
```bash
# While loop with counter
while [ "$LOCATION_TIMEOUT" -gt 0 ]; do
# ...
LOCATION_TIMEOUT=$((LOCATION_TIMEOUT - 1))
done
# For loop over arguments
for arg in "$@"; do
if [ "$arg" = "--search-here" ]; then
search_here=true
fi
done
```
## Functions
Define functions without the `function` keyword:
```bash
locate() {
# spawn termux-location processes
(
$LOCATION_CMD -p "network" >"$SPOW_NETLC_OUT"
if [ -s "$SPOW_NETLC_OUT" ]; then
printf "net"
fi
) &
tl_net_pid="$!"
}
```
## Process Management
### Background Processes
Spawn background processes in subshells:
```bash
(
$LOCATION_CMD -p "network" >"$SPOW_NETLC_OUT"
if [ -s "$SPOW_NETLC_OUT" ]; then
printf "net" | tee -a "$SPOW_SESH_ERR"
fi
) &
tl_net_pid="$!"
```
### Waiting for Processes
Check if process is still running:
```bash
kill -0 "$tl_net_pid" >/dev/null 2>&1
tl_net_status="$?"
if [ "$tl_net_status" -eq 1 ]; then
# Process finished
break
fi
```
## Output
### printf vs echo
Always use `printf` instead of `echo`:
```bash
# Good
printf "s+ow: error: surplus is not installed.\n"
printf "running '%s'" "$LOCATION_CMD"
# Bad (non-portable)
echo "message"
```
### Redirection
Redirect stderr appropriately:
```bash
# Suppress output
command -v "$SURPLUS_EXE" >/dev/null 2>&1
# Redirect to file
cat "$SPOW_NETLC_OUT" >>"$SPOW_SESH_OUT"
```
## Common Patterns
### Checking File Existence
```bash
# File exists
if [ -e "$file" ]; then
...
fi
# File exists and is non-empty
if [ -s "$file" ]; then
...
fi
# Directory exists
if [ -d "$dir" ]; then
...
fi
```
### String Operations
```bash
# Extract command name
TERMUX_EXE=$(echo "$TERMUX_CMD" | awk '{print $1}')
# Pattern matching with case
case "$arg" in
"--search-here")
search_here=true
;;
"--plumbing")
plumbing=true
;;
esac
```
### Arithmetic
Use POSIX arithmetic expansion:
```bash
LOCATION_TIMEOUT=$((LOCATION_TIMEOUT - 1))
# Check numeric comparison
if [ "$count" -gt 0 ]; then
...
fi
```
## Avoid Bashisms
### Don't Use
```bash
# Bash arrays - not POSIX
array=("item1" "item2")
echo "${array[0]}"
# [[ ]] test - not POSIX
if [[ "$var" == "value" ]]; then
...
fi
# $() command substitution is POSIX, but prefer it over backticks
output=$(command)
# let for arithmetic - not POSIX
let "count=count+1"
# source - not POSIX (use . instead)
source file.sh
```
### Do Use
```bash
# POSIX command substitution
output=$(command)
# POSIX test
if [ "$var" = "value" ]; then
...
fi
# POSIX arithmetic
result=$((a + b))
# POSIX source
. ./file.sh
```
## Shellcheck
Use shellcheck for linting:
```bash
# Check script
shellcheck script.sh
# Disable specific warnings with comments
# shellcheck disable=SC2059
LOCATION_FALLBACK="${LOCATION_FALLBACK:-"%d%d%d\nSingapore?"}"
```
## Testing Skills
- Run `shellcheck script.sh` - Should produce no warnings/errors
- Test on multiple shells: `dash script.sh`, `bash script.sh`, `sh script.sh`
- Check exit codes: Run with invalid args, missing files, verify proper codes
- Test variable quoting: Use paths with spaces to verify no word splitting
- Verify portability: Avoid `[[`, `source`, `let`, arrays unless in Bash exception
## Integration
This skill extends `dev-standards-majo`. Always ensure `dev-standards-majo` is loaded for:
- AGENTS.md maintenance
- Universal code principles
- Documentation policies
Works alongside:
- `git-majo` — For committing shell script changes
- `writing-docs-majo` — For writing shell script documentation
- `running-windows-commands-majo` — For shell scripting on Windows (Git Bash/WSL)
- `task-planning-majo` — For planning complex shell workflows
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!