12

I wrote a command to delete all git branches without needing to write them all mannually, which is currently like this:

git branch -d $(git branch)

It does what I want, but also tries to delete branches named as files in current directory, so the output is something like this:

error: branch 'angular.json' not found.
error: branch 'assembly.xml' not found.
error: branch 'ci-settings.xml' not found.
error: branch 'coverage' not found.
error: branch 'cypress.json' not found.
error: branch 'Dockerfile.angular' not found.
error: branch 'Dockerfile.spring' not found.
error: branch 'e2e' not found.
error: branch 'examples' not found.
error: branch 'mvnw' not found.
error: branch 'mvnw.cmd' not found.
error: branch 'nginx' not found.
error: branch 'node' not found.
error: branch 'node_modules' not found.
error: branch 'package.json' not found.
error: branch 'package-lock.json' not found.
error: branch 'pom.xml' not found.
error: branch 'README.md' not found.
error: branch 'scripts' not found.
error: branch 'serve-proxy.conf.json' not found.
error: branch 'src' not found.
error: branch 'target' not found.
error: branch 'to-do.txt' not found.
error: branch 'tsconfig.json' not found.
error: branch 'web.config' not found.
error: Cannot delete branch 'dev' checked out at 'D:/Documentos/oreons/Rise/archivekeeper-ui'
Deleted branch test (was 729628a).

If I echo $(git branch), the output is like this:

$ echo $(git branch)
angular.json assembly.xml ci-settings.xml coverage cypress.json Dockerfile.angular Dockerfile.spring e2e examples mvnw mvnw.cmd nginx node node_modules package.json package-lock.json pom.xml README.md scripts serve-proxy.conf.json src target to-do.txt tsconfig.json web.config dev

Which explains the command's output. But why does $(git branch) outputs all these file names besides the git branches?

1
  • 9
    IIRC git branch marks the current branch with an asterisk - the unquoted expansion $(git branch) will then be subject to filename generation (globbing), turning the * into a list of non-hidden filenames in the current directory Jan 10, 2023 at 16:06

3 Answers 3

20

As @steeldriver mentions in the above comment, git branch has an asterisk (*) in its output to mark the currently checked-out branch.

$ git branch
* main
  test

When running it as $(git branch), that asterisk in the output is expanded to all non-hidden files and directories in the directory you're currently in.

To remove the asterisk, you can format the output by passing the --format option by only showing the short format of the refnames.

$ git branch --format='%(refname:short)'
main
test

So the command you want to use would be the following.

git branch -d $(git branch --format='%(refname:short)')
2
  • Mind blown! Good explanation.
    – MEMark
    Jan 12, 2023 at 8:34
  • 1
    And the moral is - always examine the output of a command first before using it as input to something else, especially something destructive
    – Useless
    Jan 12, 2023 at 15:01
7

Don't use git branch in scripts. Instead, use the low-level commands such as git for-each-ref:

git for-each-ref --shell --format='%(refname:short)' refs/heads | \
while read branch; do git branch -d "$branch"; done

or

git for-each-ref --shell --format='%(refname:short)' refs/heads | \
xargs git branch -d

This avoids the problem of the command from the question, because it correctly quotes parameter expansions. Not quoting a parameter expansion (or command substitution) will perform pathname expansion on the expanded value.

Since git branch is a user-facing command, it marks the current branch with a *. And as you might know, * as a globbing character/wildcard, which resolves to any 0 or more characters. When executing echo *, your shell first expands and replaces * with all (non-hidden) files in your current directory, then passes this list of files as parameters to echo.

Conclusion: always quote "dollar-expressions" (not a real term, I am talking about parameter expansions and command substitutions) to prevent field splitting and pathname expansion – unless you have very good reasons to not do so (and know what you are doing)

4
  • 1
    Downvoted as this does not answer the OP's question of why they're getting the behavior they're seeing.
    – jwodder
    Jan 11, 2023 at 21:30
  • This is a more robust solution than the currently accepted one, I think Jan 12, 2023 at 8:51
  • 3
    @jwodder I disagree with the downvote. git branch internally uses git for-each-ref, and for-each-ref is better and safer to use in scripts. My answer directly answers OP's question, but this answer is more correct as a "permanent" solution.
    – Dan
    Jan 12, 2023 at 10:11
  • Nice! I didn't know I could prevent this expansion with surrounding the expression with double-quotes. Jan 12, 2023 at 19:48
1

I use this in a shell function for essentially the same purpose, though it leaves main, master, and the current branch (marked with the asterisk as discussed in the older answer and comment). You may find it useful.

git branch |
    grep -v '^\*' |
    grep -oE '[^ ]+' |
    grep -vE '^(master|main)$' |
    while read branch_name; do
        git branch -D $branch_name
    done
2
  • 1
    Downvoted as this does not answer the OP's question of why they're getting the behavior they're seeing.
    – jwodder
    Jan 11, 2023 at 21:30
  • That was already answered in perfect detail. I'm not going to repeat that uselessly, I'm going to help OP further. Jan 18, 2023 at 19:26

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .