1

I'm learning bash scripting and needed some simple help.

Here is what I have thus far:

find . -type d -empty -not -path "./.git/*" -exec touch {}/.gitkeep \;

So what this does is starts from a root path, finds all directories inside this root path that are empty and do not have a .git folder, and then when that operation is successful it runs -exec touch {}/.gitkeep to create a file .gitkeep inside that empty directory to ensure proper git commits.

What I want now is to echo out the current file path for the gitkeep file just created.

My first question is:

Should I be piping | as so:

find . -type d -empty -not -path "./.git/*" -exec touch {}/.gitkeep | outputFilenameDisplayFunction \;

Or maybe repeat what -exec does as so:

find . -type d -empty -not -path "./.git/*" -exec touch {}/.gitkeep - exec outputFilenameDisplayFunction \;

Or maybe use >

find . -type d -empty -not -path "./.git/*" -exec touch {}/.gitkeep > outputFilenameDisplayFunction \;

None of these commands has been tested yet. I really am looking for explanations so i can be knowledgeable in the future.

Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
somejkuser
  • 8,856
  • 20
  • 64
  • 130
  • Re: `> outputFilenameDisplayFunction` -- redirections are interpreted *by the shell* before it even starts `find`. Consequently, it's not part of the `-exec` sequence (when you have a shell that honors them in non-head-or-tail position, as bash does); and even if you quoted it, as in `'>outputFilenameDisplayFunction'`, such an idiom is a *shell redirection*; when you have no shell at all (and `-exec` doesn't start a new shell on its own), nothing is there to honor it. – Charles Duffy Mar 11 '18 at 03:35
  • ...btw, if what you really want is to pipe something to a function -- because `-exec` starts a program directly (with no shell), it can't directly start a shell function (which, by nature, only exists when you *have a shell* in the first place). You *can* run a shell function under `-exec` if you export it with `export -f` and ensure that the child shell is bash, but that's a fair bit of work, and generally not the best approach for the job. – Charles Duffy Mar 11 '18 at 03:41
  • In general, I'd suggest referring to the practices in [Using Find](http://mywiki.wooledge.org/UsingFind) -- note in particular the use of NUL-delimited streams to pass names from `find` to a bash `while read` list (a technique discussed also in [BashFAQ #1](http://mywiki.wooledge.org/BashFAQ/001)). – Charles Duffy Mar 11 '18 at 03:43
  • BTW, `-exec touch '{}/.gitkeep'` isn't guaranteed to work everywhere -- the POSIX specification for `find` only promises that `{}` is honored when it's a standalone argument on its own, not when it's a substring of another argument, so operating systems with simpler `find` implementations may not behave as you intend here. – Charles Duffy Mar 11 '18 at 04:30

3 Answers3

1

As mentioned here, find accepts multiple -exec portions to the command.

In your case, the second one can call a script, as in here:

find . -type d -empty -not -path "./.git/*" -exec touch {}/.gitkeep \; -exec myscript {} \;

Note the \;.

The script would be:

#!/bin/sh

echo "$1" > "afile"

Charles Duffy actually proposes in the comments fir the second -exec:

-exec sh -c 'echo "$1" >>aFile' _ {} \;

avoid the need for an external file storing your script.

VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250
  • Keeping the `-path './.git/*'` is unfortunate -- that's a quite inefficient construct compared to using `-prune`. – Charles Duffy Mar 11 '18 at 03:32
  • @CharlesDuffy I agree. I was simply building on the base command proposed by the OP. – VonC Mar 11 '18 at 03:33
  • BTW, I believe there are some whitespace-related typos here right now -- presumably you mean to have a space before the `\;`, and no space between the following `-` and `exec`. – Charles Duffy Mar 11 '18 at 03:35
  • @CharlesDuffy Right, fixed. – VonC Mar 11 '18 at 03:35
  • LGTM, though personally, I might `-exec sh -c 'echo "$1" >>aFile' _ {} \;` or such and avoid the need for an external file storing your script. – Charles Duffy Mar 11 '18 at 03:39
  • @CharlesDuffy Good point. I missed https://stackoverflow.com/a/25507672/6309 before. – VonC Mar 11 '18 at 03:41
1

Let's start from your stated requirements:

So what this does is starts from a root path, finds all directories inside this root path that are empty and do not have a .git folder, and then when that operation is successful it runs -exec touch {}/.gitkeep to create a file .gitkeep inside that empty directory to ensure proper git commits.

If a directory is empty, it "can't have a .git folder" in the sense of having a child named .git by definition -- if it had any subdirectory, it wouldn't be empty. So we can completely ignore that part of your description in prose -- or interpret to refer to what the code actually appears to be intended to do, pruning any directory which is under .git.

Should that be your intent, -path is the wrong tool for that job altogether, as it still searches the .git tree (and then excludes all the things that it found); instead, use -prune to stop find from recursing down that path at all:

while IFS= read -r -d '' dirname; do
  touch -- "${dirname}/.gitkeep"
  printf '%q\n' "$dirname" # this goes to the logfile, since we open it for the whole loop
done < <(find . -name .git -prune -o -type d -empty -print0) >logFile

Why prefer this approach?

  • Instead of starting a shell per directory found (as would happen if you used -exec to start a shell script or a shell), it keeps your initial/primary shell running, and iterates through the loop once per item found.
  • Because it's running code in that shell, you can use shell functions; modify shell variables (as with (( ++directoriesFound )) to keep a counter, f/e), or perform redirections scoped to the loop (ie. >logFile) to open an output file just once and use if repeatedly within.
Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
1

On GNU/anything, find has -printf, which makes doing what you want a straight

find -name .git -prune \
  -o -type d -empty -printf %p/.gitkeep\\n -execdir touch {}/.gitkeep \;

(note: fixed omitted {}/, and GNU find's -execdir doesn't change the behavior here but is safer than -exec on systems that may find themselves under attack, the exec'd command is run directly in the location find got to rather than causing the executed command to re-walk the path).

jthill
  • 55,082
  • 5
  • 77
  • 137