1

I have the following command that I am trying to put into a bash alias. The command by itself works fine, but when I try to alias it, I am getting the following errors:

The Command

find . -maxdepth 1 -mindepth 1 -type d -exec sh -c 'echo "$(find "{}" -type f | wc -l)" {}' \; | sort -nr

The Alias

alias csfiles='find . -maxdepth 1 -mindepth 1 -type d -exec sh -c 'echo "$(find "{}" -type f | wc -l)" {}' \; | sort -nr'

The Error:

-sh: alias 0: not found
-sh: alias {} \; | sort nr: not found

I think this means I am not using quotes right but I am having trouble determining the correct combo. Help?

hek2mgl
  • 152,036
  • 28
  • 249
  • 266
zeeple
  • 5,509
  • 12
  • 43
  • 71
  • 2
    It's been a while since I've worked with aliases but I think your alias is too complex. I believe alias is just used to shorten one command with arguments, i.e. `alias l='ls -al'`. You may need to create a function `csfiles`. But it has been a while so I may be mistaken. – Jonny Henly Nov 20 '18 at 17:59
  • see https://stackoverflow.com/questions/25941394/how-does-bash-deal-with-nested-quotes – hek2mgl Nov 20 '18 at 18:01
  • 2
    I second creating a bash function to do this. You would call it the same way, with the benefit of being able to pass arguments if needed. – ahota Nov 20 '18 at 18:01
  • 2
    As written, the `find` command will fail for certain file names (such as those containing double quotes). Don't try to embed `{}` in the command; only use it as an argument to `sh`. `sh -c '....' _ {}`. Wherever you used `{}`, use `$1`. – chepner Nov 20 '18 at 18:17
  • @chepner that's a very good point! – hek2mgl Nov 20 '18 at 18:18

2 Answers2

4

Your outer find doesn't do anything you couldn't do with a simple glob. This eliminates a layer of quotes (along with the sh process for each directory found).

# Ignoring the issue of assuming no file name contains a newline
for d in ./*/; do
   echo "$(find "$d" -type f | wc -l) $d"
done

Just define a shell function to eliminate the second layer imposed on the argument to alias.

csfiles () {
  for d in ./*/; do
    echo "$(find "$d" -type f | wc -l) $d"
  done
}

The remaining call(s) to find can also be replaced with a for loop, eliminating the problematic assumption of one line per file name:

csfiles () {
  for d in ./*/; do
    echo "$(for f in "$d"/*; do [ -f "$f" ] && echo; done | wc -l) $d"
  done
}

You could keep find if it supports the -printf primary, because you don't care about the actual names of the files, just that you get exactly one line of output per file.

csfiles () {
  for d in ./*/; do
    echo "$(find "$d" -type f -printf . | wc -l) $d"
  done
}
chepner
  • 497,756
  • 71
  • 530
  • 681
1

You can use double quotes around the definition, like this:

alias foo="find . -maxdepth 1 -mindepth 1 -type d -exec sh -c 'echo \"\$(find \"{}\" -type f | wc -l)\" {}' \; | sort -nr"

Every literal " inside the definition gets escaped: \".

Note: You also need to escape the inner command substitution to prevent it from getting expanded upon alias definition time. Like this ... \$(...)


As a follow up on chepners comment, you should pass the filename to the inner find command as an argument. Otherwise you will run into problems if one of your folders has a name with a " in it:

alias foo="find . -maxdepth 1 -mindepth 1 -type d -exec bash -c 'echo \"\$(find \"\${1}\" -type f | wc -l) \"\${1}\" \"' -- \"{}\" \; | sort -nr"
hek2mgl
  • 152,036
  • 28
  • 249
  • 266