Skip to content
shellmap

List modified files in a git repo

Print the paths of files with uncommitted changes (staged or unstaged) — for pre-commit hooks, CI lint-only-changed gates, and "what am I about to commit".

How to list modified files in a git repo in each shell

Bashunix
git status --porcelain | awk '{print $2}'

`--porcelain` is the stable machine-parseable format (`--porcelain=v1` since git 2.0; `=v2` is the richer variant). Column 1 is the 2-char status (`XY`); column 2 is the path. Renames show `R old -> new` — awk `$2` returns `old` (the arrow form breaks naive parsing — use `--porcelain=v2` for unambiguous output).

Zshunix
git status --porcelain | awk '{print $2}'
Fishunix
git status --porcelain | awk '{print $2}'
PowerShellwindows
git status --porcelain | ForEach-Object { ($_ -split "\s+", 3)[2] }

`-split "\s+", 3` splits on whitespace into AT MOST 3 fields — preserves paths containing spaces in the third field. Without the limit, "src/my file.txt" splits into 3+ pieces.

cmd.exewindows
git status --porcelain

cmd has no per-line column parser. Output the raw porcelain and let the consumer handle parsing — or shell out to pwsh.

Equivalents listed for Bash, Zsh, Fish, PowerShell, cmd.exe.

Gotchas & notes

  • `git status --porcelain` (every modified file: staged + unstaged + untracked) vs `git diff --name-only` (unstaged only) vs `git diff --cached --name-only` (staged only) — picking the wrong one yields a silently incomplete list. For "everything that's NOT in HEAD yet": `git diff HEAD --name-only` (combines staged + unstaged) PLUS `git ls-files --others --exclude-standard` (untracked). Or use `git status --porcelain` once and parse the two-char status column: `XY` where X = index status, Y = worktree status; `??` = untracked, `!!` = ignored (only with `--ignored`).
  • Pathname safety: file paths can contain spaces, newlines, tabs, even quotes. Naive `awk '{print $2}'` breaks on "my file.txt" (awk's default FS is whitespace → splits on the space). The robust portable form: `git status --porcelain -z | tr '\0' '\n'` (the `-z` flag uses NUL separators between records AND between rename-old/new pairs, which `tr` then renders one-per-line). For shell scripts that need to iterate: `while IFS= read -r -d "" entry; do ...; done < <(git status --porcelain -z)`. NEVER pipe paths through shell expansion (`for f in $(git ...)`) — word-splitting eats the whitespace.
  • **Renames** in `--porcelain` v1: `R old -> new` (space-arrow-space separator). In `--porcelain=v2` (git 2.11+): one line per change, fields are `2 <XY> <sub> <mH> <mI> <mW> <hH> <hI> <X><score> <path>\t<origPath>` — strictly tab-separated, no in-string arrow. The v2 format is the right choice for any new tooling. Concretely: `git status --porcelain=v2 -z | awk -v RS='\0' '$1==2 { print substr($0, index($0, FS)+1) }'` handles renames cleanly.
  • Filtering to just one status: `git diff --name-only --diff-filter=M` (only Modified, no Added/Deleted/Renamed); `--diff-filter=AM` (Added or Modified); uppercase = include, lowercase = exclude (`--diff-filter=d` = "all except deleted"). Useful for lint-on-touched-files CI gates: `git diff --name-only origin/main --diff-filter=AM -- "*.ts" | xargs eslint`. pwsh: pipe through `Where-Object { $_ -match "\.ts$" }` rather than glob inside git.

Related commands

Related tasks