< Back to all hacks

#04 Git Argument Parser

Infrastructure
Problem
npm passes git arguments in unexpected order that the simple wrapper doesn't handle.
Solution
Enhanced wrapper with argument parsing that extracts URL and destination regardless of order.
Lesson
npm's git invocations are not documented and change between versions. Parse arguments defensively, never assume positional order.

Context

Inside the proot environment, the real git binary is either unavailable or too old. A bash wrapper script at /usr/bin/git intercepts calls and translates them, performing SSH-to-HTTPS URL rewriting (hack 11) and routing operations to the host Termux git binary.

The initial wrapper assumed git clone <url> <dest> positional ordering. This broke immediately because npm calls git in at least five different argument patterns during npm install:

  • git clone <url> <dest>
  • git clone --depth 1 <url> <dest>
  • git clone <url> --depth 1 <dest>
  • git clone --mirror <url>
  • git clone -c advice.detachedHead=false --depth 1 <url> <dest>

The wrapper must handle all of these without knowing in advance which pattern npm will use. This is a LEGACY hack, preserved for reference after the native migration eliminated proot.

Implementation

The wrapper script uses a state-machine argument parser that categorizes each argument:

#!/bin/bash
# /usr/bin/git — proot git wrapper with argument parser

REAL_GIT="/data/data/com.termux/files/usr/bin/git"
SUBCOMMAND="$1"
shift

if [ "$SUBCOMMAND" = "clone" ]; then
  URL=""
  DEST=""
  FLAGS=()

  while [ $# -gt 0 ]; do
    case "$1" in
      --depth|--branch|-b|-c|--reference|--origin|-o)
        # Flags that consume the next argument
        FLAGS+=("$1" "$2")
        shift 2
        ;;
      --mirror|--bare|--recursive|--recurse-submodules|--shallow-submodules|--no-checkout)
        # Standalone flags
        FLAGS+=("$1")
        shift
        ;;
      -*)
        # Unknown flag — pass through as standalone
        FLAGS+=("$1")
        shift
        ;;
      *)
        # Positional argument: first is URL, second is DEST
        if [ -z "$URL" ]; then
          URL="$1"
        elif [ -z "$DEST" ]; then
          DEST="$1"
        fi
        shift
        ;;
    esac
  done

  # Rewrite SSH URLs to HTTPS (see hack 11 for details)
  URL=$(echo "$URL" | sed \
    -e 's|^git+ssh://git@github.com/|https://github.com/|' \
    -e 's|^git+ssh://git@github\.com:|https://github.com/|' \
    -e 's|^ssh://git@github.com/|https://github.com/|' \
    -e 's|^git@github\.com:|https://github.com/|')

  # Normalize .git suffix
  URL="${URL%.git}.git"

  if [ -n "$DEST" ]; then
    exec "$REAL_GIT" clone "${FLAGS[@]}" "$URL" "$DEST"
  else
    exec "$REAL_GIT" clone "${FLAGS[@]}" "$URL"
  fi
else
  # All non-clone subcommands pass through unchanged
  exec "$REAL_GIT" "$SUBCOMMAND" "$@"
fi

Deploy the wrapper inside proot:

# From Termux
ROOTFS="$PREFIX/var/lib/proot-distro/installed-rootfs/ubuntu"
cp /path/to/git-wrapper.sh "$ROOTFS/usr/bin/git"
chmod +x "$ROOTFS/usr/bin/git"

Verification

Test each argument ordering pattern:

# Standard order
proot-distro login ubuntu -- git clone https://github.com/user/repo /tmp/test1

# Flags before URL
proot-distro login ubuntu -- git clone --depth 1 https://github.com/user/repo /tmp/test2

# Flags between URL and dest
proot-distro login ubuntu -- git clone https://github.com/user/repo --depth 1 /tmp/test3

# Flag-value pair before URL
proot-distro login ubuntu -- git clone -c advice.detachedHead=false --depth 1 \
  https://github.com/user/repo /tmp/test4

# Mirror (no dest)
proot-distro login ubuntu -- git clone --mirror https://github.com/user/repo
  • All five patterns should succeed without "repository not found" errors.
  • npm install inside proot completes git-dependency resolution for packages with git deps.
  • Adding set -x to the wrapper and running npm install shows the parsed URL and FLAGS for each invocation.

Gotchas

  • The -c flag takes a key=value argument that looks positional (e.g., advice.detachedHead=false). Without special handling in the case statement, the parser treats it as the URL. Every flag that consumes a following argument must be explicitly enumerated.
  • npm may add new flags in future versions. The -* catch-all handles unknown standalone flags, but new flags that consume a following argument will break parsing silently. Keep the known-flag list updated when npm upgrades.
  • The exec at the end is important. Without it, the wrapper forks a child process and the parent shell stays alive, wasting a PID and a small amount of RAM on a 1 GB device.
  • Do not quote "${FLAGS[@]}" as a single string. The [@] form with double quotes correctly preserves each array element as a separate word. Using "${FLAGS[*]}" would concatenate everything into one argument.
  • Termux's sed behaves differently from GNU sed in edge cases. The URL rewriting patterns are intentionally simple (fixed string prefixes) to avoid portability issues.
  • Non-clone subcommands (git fetch, git ls-remote, git rev-parse) pass through without any parsing. npm uses these internally, and they must reach the real git binary unchanged.

Result

npm install runs to completion inside proot without git argument ordering failures. The wrapper correctly handles all observed npm git invocation patterns, rewriting SSH URLs to HTTPS and forwarding flags in the correct positions. Combined with the sed URL rewriter (hack 11), this gave proot a fully functional git layer for npm dependency resolution.