5

I'm trying to align output from a bash for loop.

Currently, I'm getting output from my loop that looks like so:

Directory:  /some/long/directory/path  Remote:  some-remote
Directory:  /some/dir/path  Remote:  other-remote

Which I'm trying to align like so:

Directory:  /some/long/directory/path  Remote:  some-remote
Directory:  /some/dir/path             Remote:  other-remote

The current, basic loop that generates this output looks something like this:

for dir in $(find /some/path -type d -name .git); do
    cd $dir
    remote=$(git remote)
    printf "Directory: $dir\tRemote: $remote\n
done

I've tried using:

  • column (which formats each line separately, as it's a for loop)
  • printf (printf "Directory: %s Remote: %s\n" "$dir" "$remote")
  • awk (echo "Directory: $dir Remote: $remote" | awk '{printf ("%s-20s %s-20s %s-20s %s-20s",$1 $2 $3 $4)}')

Among many other variations of these commands.

I'm probably missing something basic (I tried my best at looking at other examples online and reading the man pages), but I couldn't get it to work.

I'd really appreciate any pointers as to what I'm doing wrong.

Jeff Schaller
  • 67,283
  • 35
  • 116
  • 255
PGEL
  • 137

3 Answers3

6

column should work just fine. However, you don't add it to each loop iteration, but in the end:

for
...
done | column -t

Output:

Directory:  /some/long/directory/path  Remote:  some-remote
Directory:  /some/dir/path             Remote:  other-remote

Some additional notes regarding your script:

  • Do not loop find output like this. Check here.
  • Quote file/directory variables --> cd "$dir"
  • Do not use Variables in printf FORMAT string. --> printf 'Directory: %s\tRemote: %s\n' "$dir" "$remote"
pLumo
  • 22,565
5

You can use column, but

Since you indicate that your shell is bash, you can instead use the globstar and nullglob (and, as noted by @Kusalananda, the dotglob) options to iterate directly from the shell:

shopt -s globstar
shopt -s nullglob
shopt -s dotglob
for d in **/.git/
do
  cd "$d"
  printf "Directory: %s\tRemote: %s\n" "${d%/.git/}" "$(git remote)"
  cd - >/dev/null
done | column -t -s $'\t'
  • The globstar option enables the ** glob (aka "wildcard") that matches any depth of intermediate directories, thereby allowing you to descend into the directory tree within a loop.
  • The nullglob option will ensure that you don't iterate at all if there is no matching directory. Without it, the glob pattern will be taken as refering to a directory with literal name **/.git/ in case no filename matches, and the loop will be executed once with this (non-sensical) value for $d.
  • The dotglob option will ensure that the ** also matches "hidden" intermediate directories (i.e. those that begin with a ., as in some/directory/.path/repository).
  • Since you don't need the .git/-part of the path, the shell string processing directive ${d%/.git/} will remove the last appearence of /.git/ from the value of $d.
  • The output within the loop is TAB-separated, so the column command is instructed via -s $'\t' to take a TAB character as input column separator rather than the default (which is space-separated columns).
AdminBee
  • 22,803
  • I wasn't aware of this - thank you for letting me know. If I manage to format a loop without find, I'll be sure to post it here. – PGEL Jul 27 '21 at 07:47
4

Rewriting your loop:

find /some/path -type d -name .git -exec sh -c '
    for dirpath do
        printf "Directory: %s@Remote: %s\n" \
            "${dirpath%/.git}" \
            "$( git -C "$dirpath" remote )"
    done' sh {} + |
column -s '@' -t

This uses column to format the output of find. The find command finds all .git directories and outputs the needed info with the help of an in-line sh -c script that is called with batches of found directory paths. For output, the in-line script modifies the given directory paths to not include the actual /.git at the end, which means they would instead point to the Git project directory.

The output is tabulated with @ characters, which column later uses to aligned the data. Change the @ in the in-line sh -c script and in the call to column if you need to use another character.

Related:


If you have Git repositories with multiple remotes, you may want to duplicate the output for each remote separately:

find /some/path -type d -name .git -exec sh -c '
    for dirpath do
        git -C "$dirpath" remote |
        while IFS= read -r remote; do
            printf "Directory: %s@Remote: %s\n" \
                "${dirpath%/.git}" "$remote"
        done
    done' sh {} + |
column -s '@' -t

Here, we simply read lines from git -C "$dirpath" remote. Each line will contain the name of a Git remote, and for each read remote we do our output.

This may output

Directory: /some/path/src  Remote: origin
Directory: /some/path/src  Remote: private

if the repository at /some/path/src has two remotes.


Just for fun, we want JSON output:

find /some/path -type d -name .git -exec sh -c '
    for dirpath do
        git -C "$dirpath" remote |
        while IFS= read -r remote; do
            jo directory="${dirpath%/.git}" remote="$remote"
        done
    done' sh {} +

Possible output:

{"directory":"/some/path/src","remote":"origin"}
{"directory":"/some/path/src","remote":"private"}
{"directory":"/some/path/yash-shell","remote":"origin"}
{"directory":"/some/path/zsh-shell","remote":"origin"}
{"directory":"/some/path/datamash","remote":"origin"}
Kusalananda
  • 333,661