6

Is there a way to find the length of the array *(files names) in zsh without using a for loop to increment some variable?

I naively tried echo ${#*[@]} but it didn't work. (bash syntax are welcome as well)

Cristiano
  • 63
  • 1
  • 5
  • 1
    By "length of the array" do you mean its number of elements? And do you want to get this without actually defining this array (`*` suggest you want to use shell globing mechanism here)? – jimmij Feb 09 '19 at 01:36
  • Oops, you're right i should have asked the other way around, I'll edit it. – Cristiano Feb 09 '19 at 01:48
  • @Cristiano: zsh doesn't have anything to do with it. `*` is not an array in the way you are using it, it is a shell glob. Arrays have nothing to do with your question unless you create an array as Jeff did in his answer. Your question is "How do I find how many files are in the current directory" – jesse_b Feb 09 '19 at 01:49
  • @Jess_b But it acts like an array don't you think? `echo *[0]` in zsh prints the 1st file name... – Cristiano Feb 09 '19 at 02:07
  • @Cristiano: I believe that is a zsh specific glob qualifier but still doesn't make the glob an array – jesse_b Feb 09 '19 at 02:15
  • re-edit your question and answer @jimmij 's question then your question will be reviewed again. – X Tian Feb 10 '19 at 18:40

2 Answers2

6

${#*[@]} would be the length of the $* array also known as $@ or $argv, which is the array of positional parameters (in the case of a script or function, that's the arguments the script or function received). Though you'd rather use $# for that.

* alone is just a glob pattern. In list context, that's expanded to the list of files in the current directory that match that pattern. As * is a pattern that matches any string, it would expand to all file names in the current directory (except for the hidden ones).

Now you need to find a list context for that * to be expanded, and then somehow count the number of resulting arguments. One way could be to use an anonymous function:

() {echo There are $# non hidden files in the current directory} *(N)

Instead of *, I used *(N) which is * but with the N (for nullglob) globbing qualifier which makes it so that if the * pattern doesn't match any file, instead of reporting an error, it expands to nothing at all.

The expansion of *(N) is then passed to that anonymous function. Within that anonymous function, that list of file is available in the $@/$argv array, and we get the length of that array with $# (same as $#argv, $#@, $#* or even the awkward ksh syntax like ${#argv[@]}).

ilkkachu
  • 133,243
  • 15
  • 236
  • 397
Stéphane Chazelas
  • 522,931
  • 91
  • 1,010
  • 1,501
  • Would you actually call `$*` an array at all? A "string" would be a better word for it, IMHO. Maybe it's different in `zsh`? – Kusalananda Feb 10 '19 at 07:58
  • @Kusalananda how could it be a string if the elements it's made of are words/strings themselves? When `$*` is used inside double quotes, the words it's made of (`$1`, `$2`, ...) will be joined by the first char of `IFS` without its elements being split on `IFS` or spaces before that: `(set -- 'a/b' 'a b'; IFS=:/; echo "<$*>")` (this latter digression is to illustrate that `$*` is not somehow a string split an then rejoined in this particular syntax). –  Feb 10 '19 at 10:05
  • @mosvy I know he mentions `$*` and `$@` unquoted, but `"$*"` is at least _clearly_ a string whereas `"$@"` is not a single string. Calling `$*` an array and saying another name for it is `$@` seems to confuse the purpose of these two variables. – Kusalananda Feb 10 '19 at 10:11
  • @Kusalananda, it's the array of positional parameters. `zsh` and `yash` have real arrays and arrays whose indice start at 1, so they can represent that array as a normal array variable. That array goes by the `*`, `@` and `argv` names in `zsh`. Except that `$@` is special inside double quotes on list contexts like it was in the Bourne shell. `$*` inside double quotes expands to the concatenation of the elements like `$argv` or `$argv[*]` (or even `$*[*]`) do, but that doesn't make it less that `$argv` is an array variable, not a scalar variable. – Stéphane Chazelas Feb 17 '19 at 18:02
5
files=(*)
printf 'There are %d files\n' "${#files[@]}"

or

set -- *
printf 'There are %d files\n' "$#"

You have to name the array first (as I did above with files) or use the built-in array $@ by populating it with the wildcard, as I did in the second example. In the former, the "length" (number of files) of the array is done with the ${#arrayname[@]} syntax. The number of elements in the built-in array is in $#.

Jeff Schaller
  • 66,199
  • 35
  • 114
  • 250
  • So the `* ` acts as a regex expression? I thought that it was a special array... although surprise me that there is no such array... – Cristiano Feb 09 '19 at 01:59
  • wait, in zsh `*[0] or *[1] ` gives me some file name... now I'm puzzled. – Cristiano Feb 09 '19 at 02:02
  • The array is $@... $* equals a single string with each argument separated by $IFS (usually space). ZSH and Bash doesn't handle usage of an "index" in space-delimited strings the same way. – svin83 Feb 09 '19 at 03:19
  • Cristiano, the `*[0]` in zsh would expand to filenames that start with anything (or nothing) followed by any of the following characters: `0`. The square brackets designates a set of characters in that position. You likely have files that end with a zero in your current directory. To tell zsh to give you back the first element of a `*` wildcard, you'd use parenthesis for array syntax: `print -l *([1])` -- noting that zsh arrays start at 1 unless you set `$KSH_ARRAYS` – Jeff Schaller Feb 09 '19 at 17:14