Why do I need to use cd "$@" instead of cd "$1" when writing a wrapper for cd?

Because, according to bash(1), cd takes arguments

   cd [-L|[-P [-e]] [-@]] [dir]
          Change  the  current  directory to dir.  if dir is not supplied,
          ...

so therefore the directory actually may not be in $1 as that could instead be an option such as -L or another flag.

How bad is this?

$ cd -L /var/tmp
$ pwd
/var/tmp
$ cd() { builtin cd "$1"; }
$ cd -L /var/tmp
$ pwd
/home/jhqdoe
$ 

Things could go very awry if you end up not where you expect using cd "$1"


Using "$@" will pass all arguments to cd where as $1 will only pass the first argument.

In your examples

$ . cdtest.sh "r st"

always works as you only pass in one argument, but if you were to pass in a flag as well such as

$ . cdtest.sh -L "r st"

Then only "$@" will execute correctly where "$1" will expand to cd -L losing the directory entirely.

However

$ . cdtest.sh r st

Fails in both cases as you are passing two parameters to cd, r and st which is not a valid way to execute cd. Parameters are separated by spaces which must be quoted (as in your first example) or escaped (r\ st) to be treated as one argument.

In the case of cd however it is very uncommon to pass in flags and you cannot pass in multiple directories so you will not see the difference in a real world use of either "$1" or "$@" for cd. But for other commands you will notice a difference so it is best practice to always use "$@" when you want to create a wrapper function or script like this.


There's also the case when there are no arguments:

$ cd /tmp; cd; pwd
/home/muru
$ cd_func() { builtin cd "$1"; }
$ cd_func /tmp; cd_func; pwd
/tmp

cd without any arguments changes to the home directory. Without any arguments, "$@" expands to nothing, but "$1" expands to the empty string. These are different:

$ args() { for i in "$@"; do echo "|$i|"; done; }
$ args
$ args ""
||