Bash Arguments

TLDR: Default to "${@}" for argument passing between functions in shell.

Content

Typically "${@}" is what you want when you want to pass arguments from one bash function to another.

@ symbol retains the separation between arguments while * puts them together.

Example code

foo() {
  local num_args="$#"
  echo "Number of arguments: $num_args"
  echo "arg1: ${1:-<empty>}"
  echo "arg2: ${2:-<empty>}"
  echo "arg3: ${3:-<empty>}"
}

# shellcheck disable=SC2016
main() {
  echo.bold '"${@}"'
  foo "${@}"

  echo.bold '"${*}"'
  foo "${*}"
}

main "${@}" || exit 1

When we run this with separate arguments we see that "${*}" has combined arguments into 1.

❯sr 1 2 3
"${@}"
Number of arguments: 3
arg1: 1
arg2: 2
arg3: 3

"${*}"
Number of arguments: 1
arg1: 1 2 3
arg2: <empty>
arg3: <empty>

This rule is part of SC2048 · koalaman/shellcheck Wiki

Using unquoted star $* in bash to pass arguments easily leads to arguments being incorrectly split up based on white space, even when quotes are used.

Here is an example, where the quoted argument "1a 1b" gets split up into separate arguments.

foo() {
  local num_args="$#"
  echo "Number of arguments: $num_args"
  echo "arg1: ${1:-<empty>}"
  echo "arg2: ${2:-<empty>}"
  echo "arg3: ${3:-<empty>}"
}

# shellcheck disable=SC2016
main() {
  echo.bold '"${@}"'
  foo "${@}"

  echo.bold '"${*}"'
  foo ${*}
}

main "${@}" || exit 1

Output running the above:

❯sr "1a 1b" 2
"${@}"
Number of arguments: 2
arg1: 1a 1b
arg2: 2
arg3: <empty>

"${*}"
Number of arguments: 3
arg1: 1a
arg2: 1b
arg3: 2

Example processing pipe line by line

Test case

foo() {
  if [ -t 0 ]; then
    echo "Called normally"
    echo "Received arg: $1"
  else
    echo.green "Called from a pipeline"

    while IFS= read -r line; do
      echo "Processing piped input: $line"
    done
  fi
}
export -f foo


main() {
  echo "Calling foo() normally"
  foo "bar"
  echo "--------------------------------------------------------------------------------"
  echo "Calling foo() from a pipeline"
  echo "bar" | foo
}

main "${@}" || exit 1

Output:

Calling foo() normally
Called normally
Received arg: bar
--------------------------------------------------------------------------------
Calling foo() from a pipeline
Called from a pipeline
Processing piped input: bar

real	0m0.016s
user	0m0.013s
sys	0m0.004s
Compress piped input with JQ
# shellcheck disable=SC2120
bar(){
  if [ -t 0 ]; then
    echo "BAR Called normally: $*"
  else
    echo "BAR Called from a pipeline"

    while IFS= read -r line; do
      echo.bold "BAR Processing piped input: $line"
      sleep 2
    done
  fi
}

foo() {
  if [ -t 0 ]; then
    echo "${*:?}" | jq -c | bar
  else
    jq -c | bar
  fi
}
export -f foo

main() {
  echo.green "$(date.now)"
  echo.jsonl | jq | foo
  echo.green "$(date.now)"
  foo '{"hi":"there"}'
}

main "${@}" || exit 1

Output:

2023-12-14T21-17-51PST
BAR Called from a pipeline
BAR Processing piped input: {"title":"title-val-1","note":"note-val-1"}
BAR Processing piped input: {"title":"title-val-2","note":"note-val-2"}
BAR Processing piped input: {"title":"title-val-3","note":"note-val-3"}
2023-12-14T21-17-57PST
BAR Called from a pipeline
BAR Processing piped input: {"hi":"there"}


Children
  1. Don't use unquoted star [${*}, $*]
  2. Typically At-Symbol (@) Is Better Than Star (*) for argument passing in Shell