shellharden

repository·master·Indexed 26 days ago

https://github.com/anordal/shellharden

A syntax highlighter and automation tool (version 4.3.2) designed to rewrite shell scripts for ShellCheck conformance. It focuses on proper quoting to prevent common bash pitfalls like word splitting and indirect pathname expansion. It can be used to suggest quoting changes via colors or automatically apply them using the --transform flag.

Tokens
5.8K
Snippets
14
Records
34
Agent score
85%

What's inside shellharden

  1. Use Shellharden for syntax highlighting and quoting suggestions

    master

    By default, Shellharden operates like cat, providing syntax highlighting in foreground colors and suggesting quoting changes using background colors:

    • Foreground colors: Syntax highlighting.
    • Background colors (green/red): Indicates characters that Shellharden would add (green) or remove (red) if the --transform option were used.

    Apply changes automatically

    To automatically rewrite a script to follow ShellCheck-style quoting conformance, use the --transform flag.

    WARNING: Do not apply --transform blindly. A script that relies on unquoted behavior (like implicit word splitting or glob expansion) will break after transformation. Always perform a manual code review after using this flag.

  2. Use dollar-parenthesis for command substitution

    master
    While backticks (`cmd`) are a valid way to perform command substitution, they are harder to use correctly because they require complex escaping when nested. It is recommended to use the $(cmd) syntax instead. Shellharden will automatically rewrite unquoted backticks into the dollar-parenthesis form where appropriate.
  3. Install Shellharden

    master

    You can install Shellharden using cargo from the official Rust package registry, or build it from source.

    Via Cargo

    cargo install shellharden

    From Source

    1. Build the release binary:
      cargo build --release
    2. Move the binary to your local bin directory:
       ```bash
    mv target/release/shellharden ~/.local/bin/
    cargo install shellharden
  4. Perform safe empty string comparisons

    master

    Avoid using -z or -n flags for string comparisons as they are less readable and can be ambiguous. Instead, use explicit string equality/inequality with quotes to ensure safety and readability.

    Recommended (POSIX compliant):

    test "$s" != ""
    test "$s" = ""
    [ "$s" != "" ]
    [ "$s" = "" ]

    Avoid (unreliable/unreadable):

    test -n "$s"
    test -z "$s"
    test -n $s  # Always true if $s is unset or empty
    test "$s" != ""
    test "$s" = ""
    [ "$s" != "" ]
    [ "$s" = "" ]
  5. Iterate over command output using while loops

    master

    Avoid using for i in $(command) because it relies on word splitting and can behave unexpectedly with spaces. Also, avoid piping into a loop (e.g., command | while read ...) because the pipe creates a subshell, preventing the loop from modifying variables in the parent scope.

    Instead, use process substitution with a while read loop to keep the loop in the current shell context.

    # Correct way to iterate over command output
    while read -r i; do
        printf '%s\n' "$i"
    done < <(seq 1 10)
  6. Always use quotes in Bash

    master

    To avoid vulnerabilities like word splitting and indirect pathname expansion, always wrap variable expansions and command substitutions in double quotes.

    Word Splitting: Occurs when an unquoted variable is expanded, causing the shell to split the string into multiple arguments based on the characters in $IFS (whitespace by default). Indirect Pathname Expansion: Occurs when unquoted expansions contain wildcards (* or ?), causing the shell to expand them into matching filenames.

    Exceptions where quoting is generally unnecessary:

    • Variables with invariably numeric content: $?, $$, $!, $#, and array length ${#array[@]}.
    • Assignments: a=$b.
    • The case command: case $var in ... esac.
    • Context within double brackets: [[ ... ]].
  7. Replace `echo` with `printf` for safe output

    master

    The echo command is fundamentally flawed because it interprets leading arguments as options and provides no way to suppress this parsing (unlike the -- delimiter in many other tools). To ensure data is printed exactly as intended, use printf.

    Recommended pattern: Use printf '%s\n' "$var" to print a string followed by a newline.

    Comparison:

    TaskBad (echo)Good (printf)
    Print variableecho "$var"printf '%s\n' "$var"
    Print with no newlineecho -n "$var"printf '%s' "$var"
    Print multiple argsecho "$a" "$b"printf '%s %s\n' "$a" "$b"

    If you want a reusable function, define a println helper:

    println() {
        printf '%s\n' "$*"
    }
    printf '%s\n' "$var"
    printf '%s' "$var"
    printf '%s\r' "$var"
    printf '%s %s\n' "$a" "$b"
    printf '%s\n' "${array[*]}"
    
    println() {
        printf '%s\n' "$*"
    }
  8. Combine conditions unambiguously

    master

    Avoid using the internal test operators -a (AND) and -o (OR) because they can lead to ambiguity if string content evaluates to operators. Instead, use the shell's native conjunction and disjunction operators (&&, ||, !) to combine separate test or [ commands.

    Incorrect (Ambiguous):

    test ! -e "$f" -a \( "$s" = yes -o "$s" = y \)

    Correct (Unambiguous):

    ! test -e "$f" && { test "$s" = yes || test "$s" = y; }
    ! [ -e "$f" ] && { [ "$s" = yes ] || [ "$s" = y; }
  9. Invoke commands safely from other languages

    master

    When calling shell commands from languages like Python, C++, or Node.js, avoid implicitly invoking the shell by passing a single string. This requires manual escaping and quoting, which is error-prone. Use one of these three strategies:

    1. Avoid the shell (Preferred)

    Pass arguments as an array/list directly to the OS. This bypasses shell parsing entirely.

    • Python: Use subprocess.check_call(['rm', '-rf', path]) instead of subprocess.check_call('rm -rf ' + path).
    • C/POSIX: Use posix_spawnp with an array of arguments.

    2. Static shellscript

    If shell features (like redirection or pipes) are required, embed a static shellscript and pass the dynamic data as positional arguments ($0, $1, etc.).

    • Example (Python): subprocess.check_call(['docker', 'exec', instance, 'bash', '-ec', 'printf %s "$0" > "$1"', content, path]).

    3. String processing (Last resort)

    If you must use a single string (e.g., for ssh), you must quote every argument and escape characters. The safest way is to use single quotes and escape existing single quotes by replacing ' with '\''.

    • Example (Python/SSH): subprocess.check_call(['ssh', 'user@host', "sha1sum '{}'".format(path.replace("'", "'\''"))])
  10. Handle `local` and `export` assignments safely in Bash

    master

    When using set -e (errexit), assigning a value to a builtin like local or export in a single line can cause the script to fail if the command substitution fails, because the builtin itself is treated as the command being checked. To avoid this, separate the declaration from the assignment.

    Incorrect:

    set -e
    local jobs="$(nproc)"

    Correct:

    set -e
    local jobs
    jobs="$(nproc)"
    set -e # Fail if nproc is not installed
        local jobs
        jobs="$(nproc)"
        make -j"$jobs"
  11. Use curly braces for variable substitution and interpolation

    master

    Curly braces {} serve two main purposes in Bash:

    1. Variable Substitution: Required for specific operations like parameter expansion (e.g., "${image%.png}.jpg").
    2. String Interpolation: Used to delimit the end of a variable name from subsequent characters in a string literal (e.g., "${var}string literal").

    Note on Numbered Arguments: For positional parameters greater than 9, braces are mandatory (e.g., ${10}). Shellharden permits braces on all numbered arguments to avoid errors.

  12. Check if a variable exists (POSIX vs Bash)

    master

    To check if a variable is set without relying on non-POSIX features like [[ -v var ]], use parameter expansion to provide a default value. This works even when set -u (nounset) is enabled.

    POSIX approach (Default values):

    • ${var-val}: Expands to val if var is unset.
    • ${var:-val}: Expands to val if var is unset or empty.

    Bash-specific approach:

    • [[ -v var ]]: Returns true if var is set.

    Avoid: Using test -n "$var" or [ -n "$var" ] to check existence, as these do not distinguish between a variable being unset and a variable being an empty string.