shell-scripting

安装量: 55
排名: #13527

安装

npx skills add https://github.com/sammcj/agentic-coding --skill shell-scripting
Bash Scripting Best Practices
Guidance for writing reliable, maintainable bash scripts following modern best practices. Emphasises simplicity, automated tooling, and defensive programming without over-engineering.
When to Use Shell (and When Not To)
Use Shell For:
Small utilities and simple wrapper scripts (<100 lines)
Orchestrating other programmes and tools
Simple automation tasks
Build/deployment scripts with straightforward logic
Quick data transformation pipelines
Do NOT Use Shell For:
Complex business logic or data structures
Performance-critical code
Scripts requiring extensive error handling
Anything over ~100 lines or with non-straightforward control flow
When you need proper data structures beyond arrays
Critical
If your script grows too large (1000+ lines) or complex, consider offering to rewrite it in a proper language (Python, Go, etc.) before it becomes unmaintainable. Mandatory Foundations Every bash script must have these elements: 1. Proper Shebang

!/usr/bin/env bash

Why
Portable across systems where bash may not be at /bin/bash (e.g., macOS, BSD, NixOS). Alternative :

!/bin/bash

if you know the script only runs on Linux and prefer explicit paths.
2. Strict Mode
set
-euo
pipefail
What each flag does:
-e
Exit immediately if any command fails (non-zero exit)
-u
Treat unset variables as errors
-o pipefail
Pipe fails if ANY command in pipeline fails (not just the last)
When to add
-x
Only for debugging, not in production scripts (makes output noisy). 3. ShellCheck Compliance Run ShellCheck on EVERY script before committing: shellcheck script.sh Fix all warnings. ShellCheck catches: Unquoted variables Deprecated syntax Common bugs and pitfalls Portability issues 4. Basic Script Structure

!/usr/bin/env bash

set -euo pipefail

Brief description of what this script does

Simple error reporting

die ( ) { echo "Error: ${1} "

&2 exit 1 }

Your code here

Core Safety Patterns
Always Quote Variables
Why
Prevents word splitting and globbing disasters.

Wrong - dangerous

cp $source $destination rm -rf $prefix /bin

Correct - safe

cp " ${source} " " ${destination} " rm -rf " ${prefix} /bin"

Special case: Always use braces with variables

echo " ${var} "

Good

echo " $var "

Acceptable but less consistent

echo $var

Bad - unquoted

Check Required Variables

Fail fast if required variables aren't set

: " ${REQUIRED_VAR :? REQUIRED_VAR must be set} "

Or with custom message

: " ${DATABASE_URL :? DATABASE_URL is required. Set it in .env} " Validate Inputs

Check file exists before operating on it

[ [ -f " ${config_file} " ] ] || die "Config file not found: ${config_file} "

Check command exists before using it

command -v jq

/dev/null 2

&1 || die "jq is required but not installed"

Validate directory before cd

[ [ -d " ${target_dir} " ] ] || die "Directory does not exist: ${target_dir} " Essential Patterns Pattern 1: Simple Script Template Use this for straightforward scripts:

!/usr/bin/env bash

set -euo pipefail

Description: Process log files and extract errors

die ( ) { echo "Error: ${1} "

&2 exit 1 }

Check dependencies

command -v jq

/dev/null 2

&1 || die "jq required"

Validate arguments

[ [ $# -eq 1 ] ] || die "Usage: ${0} " logfile = " ${1} " [ [ -f " ${logfile} " ] ] || die "File not found: ${logfile} "

Main logic

grep ERROR " ${logfile} " | jq -r '.message' Pattern 2: Cleanup on Exit Use trap for guaranteed cleanup:

!/usr/bin/env bash

set -euo pipefail

Create temp directory and ensure cleanup

tmpdir

$( mktemp -d ) trap 'rm -rf "${tmpdir}"' EXIT

Now use tmpdir safely - cleanup happens automatically

echo "Working in: ${tmpdir} " Pattern 3: Safe Function Definition Functions should be simple and focused:

Good: Simple, single-purpose function

check_dependency ( ) { local cmd = " ${1} " command -v " ${cmd} "

/dev/null 2

&1 || die " ${cmd} not installed" }

Good: Local variables, clear purpose

process_file ( ) { local file = " ${1} " local output = " ${2} " [ [ -f " ${file} " ] ] || die "Input file missing: ${file} "

Do processing

sed
's/foo/bar/g'
"
${file}
"
>
"
${output}
"
}
Important
Declare and set variables from command substitution separately to catch errors:

Wrong - hides errors

local result = " $( failing_command ) "

Correct - catches errors

local result result = " $( failing_command ) "

Will fail properly with set -e

Pattern 4: Safe Array Handling Arrays are useful for handling lists with spaces:

Create array

declare -a files = ( "file one.txt" "file two.txt" "file three.txt" )

Iterate safely - always quote with [@]

for file in " ${files [ @ ] } " ; do echo "Processing: ${file} " done

Build command arguments safely

declare -a flags = ( --verbose --output " ${output_file} " ) mycommand " ${flags [ @ ] } " " ${input} "

Read command output into array

mapfile -t lines < < ( grep pattern " ${file} " ) Pattern 5: Conditional Testing Use [[ ]] for bash (safer and more features):

File tests

[ [ -f " ${file} " ] ]

File exists

[ [ -d " ${dir} " ] ]

Directory exists

[ [ -r " ${file} " ] ]

File readable

[ [ -w " ${file} " ] ]

File writable

[ [ -x " ${binary} " ] ]

File executable

String tests

[ [ -z " ${var} " ] ]

String is empty

[ [ -n " ${var} " ] ]

String is not empty

[ [ " ${a} " == " ${b} " ] ]

String equality (use ==, not =)

Numeric comparison (use (( )) for numbers)

(( count

0 )) (( total = minimum ))

Combined conditions

[ [ -f " ${file} " && -r " ${file} " ] ] || die "File not readable: ${file} " Pattern 6: Simple Argument Handling For simple scripts, prefer positional arguments:

!/usr/bin/env bash

set -euo pipefail

For 1-3 arguments, just use positional parameters

[ [ $# -eq 2 ] ] || die "Usage: ${0} " source = " ${1} " dest = " ${2} " [ [ -f " ${source} " ] ] || die "Source not found: ${source} " For scripts needing flags, keep it simple:

Use environment variables instead of complex flag parsing

VERBOSE

" ${VERBOSE :- false} " DRY_RUN = " ${DRY_RUN :- false} "

Run like: VERBOSE=true DRY_RUN=true ./script.sh input.txt

Pattern 7: Process Substitution Over Temp Files Avoid creating temporary files when possible:

Instead of:

first_command

/tmp/output.txt second_command < /tmp/output.txt rm /tmp/output.txt

Use process substitution:

second_command < ( first_command )

For multiple inputs:

diff < ( sort file1.txt ) < ( sort file2.txt ) Pattern 8: Prefer Builtins Over External Commands Builtins are faster and more reliable:

Use bash parameter expansion over sed/awk for simple cases

filename

" ${path

* / } "

basename

dirname

" ${path % / *} "

dirname

extension

" ${filename

*.} "

get extension

name

" ${filename % .*} "

remove extension

Use (( )) for arithmetic over expr

count

$(( count + 1 ))

Not: count=$(expr ${count} + 1)

Use [[ ]] over [ ] or test

[ [ -f " ${file} " ] ]

Not: test -f "${file}"

Use ${#var} for string length

length

" ${

string} "

Not: length=$(echo "${string}" | wc -c)

Intermediate Patterns Pattern 9: Structured Logging Keep logging simple and consistent: log ( ) { echo "[ $( date + '%Y-%m-%d %H:%M:%S' ) ] ${1} "

&2 } error ( ) { echo "[ $( date + '%Y-%m-%d %H:%M:%S' ) ] ERROR: ${1} "

&2 }

Usage

log "Starting process" error "Failed to connect to database" Pattern 10: Main Function Pattern For longer scripts (50+ lines), use a main function:

!/usr/bin/env bash

set -euo pipefail setup ( ) {

Dependency checks, variable initialisation

command -v jq

/dev/null 2

&1 || die "jq required" } process ( ) {

Main logic here

log "Processing data" } cleanup ( ) {

Cleanup if needed

log "Cleanup complete" } main ( ) { setup process cleanup }

Call main with all script arguments

main " ${@} " Pattern 11: Idempotent Operations Scripts should be safe to run multiple times:

Check before creating

if [ [ ! -d " ${target_dir} " ] ] ; then mkdir -p " ${target_dir} " fi

Check before writing config

if [ [ ! -f " ${config_file} " ] ] ; then echo "DEFAULT_VALUE=true"

" ${config_file} " fi

Use atomic operations

mv " ${source} " " ${dest} "

Atomic on same filesystem

Pattern 12: Safe While Loop Reading Don't pipe to while (creates subshell):

Wrong - variables modified in subshell are lost

count

0 cat file.txt | while read -r line ; do (( count ++ )) done echo " ${count} "

Will be 0!

Correct - use process substitution

count

0 while read -r line ; do (( count ++ )) done < < ( cat file.txt ) echo " ${count} "

Correct count

Or use mapfile for simple cases

mapfile -t lines < file.txt count = " ${

lines
[
@
]
}
"
Style Guidelines
Formatting
Indentation
2 spaces, never tabs
Line length
Maximum 120 characters
Long strings
Use here-documents or embedded newlines

Long command - break at logical points

docker run \ --name my-container \ --volume " ${ PWD } :/data" \ --env "FOO=bar" \ my-image:latest

Long string - use here-doc

cat << EOF This is a long message that spans multiple lines and is more readable this way. EOF Naming Conventions

Functions: lowercase with underscores

check_dependencies ( ) { .. . } process_files ( ) { .. . }

Local variables: lowercase with underscores

local input_file = " ${1} " local line_count = 0

Constants/environment variables: UPPERCASE with underscores

readonly MAX_RETRIES = 3 readonly CONFIG_DIR = "/etc/myapp"

Source files: lowercase with underscores

my_library.sh

File Extensions
Executables
:
.sh
extension OR no extension (prefer no extension for user-facing commands)
Libraries
Always .sh extension and NOT executable Function Documentation Document functions that aren't obvious:

Good: Simple function, no comment needed (name says it all)

check_file_exists ( ) { [ [ -f " ${1} " ] ] }

Good: Complex function, documented

Processes log files and extracts error messages

Arguments:

$1 - Input log file path

$2 - Output directory

Returns:

0 on success, 1 on failure

process_logs ( ) { local logfile = " ${1} " local output_dir = " ${2} "

Implementation

} What to Avoid Don't Use These

Don't use backticks - use $()

output

command

Old style

output

$( command )

Correct

Don't use eval - almost always wrong

eval " ${user_input} "

Dangerous!

Don't use expr - use (( ))

result

$( expr 5 + 3 )

Old style

result

$(( 5 + 3 ))

Correct

Don't use [ ] when [[ ]] is available

[ -f " ${file} " ]

POSIX compatible, less features

[ [ -f " ${file} " ] ]

Bash, safer and more features

Don't use $[ ] for arithmetic - deprecated

result

$ [ 5 + 3 ]

Deprecated

result

$(( 5 + 3 ))

Correct

Don't use function keyword unnecessarily

function foo ( ) { .. . }

Redundant

foo ( ) { .. . }

Cleaner

Anti-Patterns

Don't glob or split unquoted

rm ${files}

DANGEROUS

rm " ${files} "

Safe

Don't use ls output in scripts

for file in $( ls ) ; do

Breaks with spaces

for file in * ; do

Correct

Don't pipe yes to commands

yes | risky-command

Bypasses important prompts

Don't ignore error codes

make build

Did it work?

make build || die "Build failed"

Better

Complexity Warning Signs If your script has any of these, consider rewriting in Python/Go: More than 100 lines Complex data structures beyond simple arrays Nested loops over arrays of arrays Heavy string manipulation logic Complex state management Mathematical calculations beyond basic arithmetic Need for unit testing individual functions JSON/YAML parsing beyond simple jq queries Advanced: Dry-Run Pattern For scripts that modify things: DRY_RUN = " ${DRY_RUN :- false} " run ( ) { if [ [ " ${DRY_RUN} " == "true" ] ] ; then echo "[DRY RUN] ${*} "

&2 return 0 fi " ${@} " }

Usage

run cp " ${source} " " ${dest} " run rm -f " ${old_file} "

Run script: DRY_RUN=true ./script.sh

Quick Reference Checklist Before considering a bash script complete: ShellCheck passes with no warnings Has proper shebang (

!/usr/bin/env bash

)
Has strict mode (
set -euo pipefail
)
All variables quoted (
"${var}"
)
Required dependencies checked
Proper error messages to stderr
Cleanup trap if using temp files
Script is idempotent where possible
Under 100 lines (or has strong justification)
Uses
command -v
not
which
Arrays used for lists with spaces
No
eval
,
ls
parsing, or backticks
Functions have local variables
Summary
Start simple
Don't over-engineer. Most scripts should be <50 lines.
Use ShellCheck
It catches most problems automatically.
Quote everything
:
"${var}"
not
$var
.
Fail fast
:
set -euo pipefail
and validate inputs.
Know when to stop
If it's getting complex, use a real language.
Compose don't complicate
Use pipes and process substitution.
Be idempotent
Scripts should be safe to run multiple times.
Test error paths
Make sure your script fails safely. Remember: Shell scripts are for gluing things together, not building complex logic. Keep them simple, safe, and focused.
返回排行榜