What does $# mean in bash?

$# is a special variable in bash, that expands to the number of arguments (positional parameters) i.e. $1, $2 ... passed to the script in question or the shell in case of argument directly passed to the shell e.g. in bash -c '...' .....

This is similar to argc in C.


Perhaps this will make it clear:

$ bash -c 'echo $#'
0

$ bash -c 'echo $#' _ x
1

$ bash -c 'echo $#' _ x y
2

$ bash -c 'echo $#' _ x y z
3

Note that, bash -c takes argument after the command following it starting from 0 ($0; technically, it's just bash's way of letting you set $0, not an argument really), so _ is used here just as a placeholder; actual arguments are x ($1), y ($2), and z ($3).


Similarly, in your script (assuming script.sh) if you have:

#!/usr/bin/env bash
echo "$#"

Then when you do:

./script.sh foo bar

the script will output 2; likewise,

./script.sh foo

will output 1.


echo $# outputs the number of positional parameters of your script.

You have none, so it outputs 0.

echo $# is useful inside the script, not as a separate command.

If you run a script with some parameters like

./instance par1 par2

the echo $# placed into the script will output 2.


$# is typically used in bash scripts to ensure a parameter is passed. Generally you check for a parameter in the beginning of your script.

For example here's a snippet of a script I was working on today:

if [[ $# -ne 1 ]]; then
    echo 'One argument required for file name, e.g. "Backup-2017-07-25"'
    echo '.tar will automatically be added as a file extension'
    exit 1
fi

To summarize $# reports the number of parameters passed to a script. In your case you passed no parameters and the reported result is 0.


Other # uses in Bash

The # is often used in bash to count the number of occurrences or the length of a variable.

To find the length of a string:

myvar="some string"; echo ${#myvar}

returns: 11

To find the number of array elements:

myArr=(A B C); echo ${#myArr[@]}

returns: 3

To find the length of the first array element:

myArr=(A B C); echo ${#myArr[0]}

returns: 1 (The length of A, 0 is first element as arrays use zero-based indices/subscripts).


$# is the number of arguments, but remember it will be different in a function.

$# is the number of positional parameters passed to the script, shell, or shell function. This is because, while a shell function is running, the positional parameters are temporarily replaced with the arguments to the function. This lets functions accept and use their own positional parameters.

This script always prints 3, regardless of how many arguments were passed to the script itself, because "$#" in the function f expands to the number of arguments passed to the function:

#!/bin/sh

f() {
    echo "$#"
}

f a b c

This is important because it means code like this does not work as you might expect, if you're not familiar with how positional parameters work in shell functions:

#!/bin/sh

check_args() { # doesn't work!
    if [ "$#" -ne 2 ]; then
        printf '%s: error: need 2 arguments, got %d\n' "$0" "$#" >&2
        exit 1
    fi
}

# Maybe check some other things...
check_args
# Do other stuff...

In check_args, $# expands to the number of arguments passed to the function itself, which in that script is always 0.

If you want such functionality in a shell function, you'd have to write something like this instead:

#!/bin/sh

check_args() { # works -- the caller must pass the number of arguments received
    if [ "$1" -ne 2 ]; then
        printf '%s: error: need 2 arguments, got %d\n' "$0" "$1" >&2
        exit 1
    fi
}

# Maybe check some other things...
check_args "$#"

This works because $# is expanded outside the function and passed to the function as one of its positional parameters. Inside the function, $1 expands to the first positional parameter that was passed to the shell function, rather than to the script it's part of.

Thus, like $#, the special parameters $1, $2, etc., as well as $@ and $*, also pertain to the arguments passed to a function, when they are expanded in the function. However, $0 does not change to the name of the function, which is why I was still able to use it to produce a quality error message.

$ ./check-args-demo a b c
./check-args-demo: error: need 2 arguments, got 3

Similarly, if you define one function inside another, you're working with the positional parameters passed to the innermost function in which the expansion is performed:

#!/bin/sh

outer() {
    inner() {
        printf 'inner() got %d arguments\n' "$#"
    }

    printf 'outer() got %d arguments\n' "$#"
    inner x y z
}

printf 'script got %d arguments\n' "$#"
outer p q

I called this script nested and (after running chmod +x nested) I ran it:

$ ./nested a
script got 1 arguments
outer() got 2 arguments
inner() got 3 arguments

Yes, I know. "1 arguments" is a pluralization bug.

The positional parameters can also be changed.

If you're writing a script, the positional parameters outside a function will be the command-line arguments passed to the script unless you have changed them.

One common way to change them is with the shift builtin, which shifts each positional parameter to the left by one, dropping the first one and decreasing $# by 1:

#!/bin/sh

while [ "$#"  -ne 0 ]; do
    printf '%d argument(s) remaining.\nGot "%s".\n\n' "$#" "$1"
    shift
done
$ ./do-shift foo bar baz      # I named the script do-shift.
3 argument(s) remaining.
Got "foo".

2 argument(s) remaining.
Got "bar".

1 argument(s) remaining.
Got "baz".

They can also be changed with the set builtin:

#!/bin/sh

printf '%d args: %s\n' "$#" "$*"
set foo bar baz
printf '%d args: %s\n' "$#" "$*"
$ ./set-args a b c d e      # I named the script set-args.
5 args: a b c d e
3 args: foo bar baz