In shell script, how to shift positional parameters of the main script using a function?

101 views Asked by At

POSIX compliant shell script does not support arrays except the positional parameters array.

Background:

In the main script, several case conditions require running the same code with a different argument. Within the repeated code, it is necessary to to shift the positional parameters.

#!/bin/sh
# /tmp/test.sh

case "$1" in
( *one*   ) echo 1 ; shift ; echo 'more' ;;
( *two*   ) echo 2 ; shift ; echo 'more' ;;
( *three* ) echo 3 ; shift ; echo 'more' ;;
( other ) echo 'other' ;;
( * ) :
esac

printf '[%s]\n' "$@"

Test it:

$ /tmp/test.sh 'arg one'  '3   spaces' "with 'quotes'"
1
more
[3   spaces]
[with 'quotes']

To minimise code repetition (the actual code contains 10+ lines), how to wrap the similar case condition codes into a function?

Trying to solve:

#!/bin/sh
# /tmp/test.sh

newline=$(printf '\n ')
newline=${newline% }

func() {
    # Perform something based on arg $1.
    # But do not echo here. echo here will mess up with the return value.
    # Actual code do not echo here.
    # echo "$1"
    shift

    shift

    # Escape dangerous characters and preserve spaces
    modified_args=""
    for arg in "$@"; do
        trailing_newlines=${arg##*[!"${newline}"]}
        modified_args="${modified_args} '$(printf "%s" "$arg" | sed "s/'/'\\\\''/g")${trailing_newlines}'"
    done
    printf '%s' "$modified_args"
}

 case "$1" in
    ( *one*   ) args=$(func 1 "$@") ; eval set -- "$args" ;;
    ( *two*   ) args=$(func 2 "$@") ; eval set -- "$args" ;;
    ( *three* ) args=$(func 3 "$@") ; eval set -- "$args" ;;
    ( other ) echo 'other' ;;
    ( * ) :
    esac


printf '[%s]\n' "$@"              

Test output:

$ newline=$(printf '\n ')
$ newline=${newline% }
$ /tmp/test.sh 'arg one'  '3   spaces' '' "with 'quotes'" 'double "quote"' "$newline line two $newline line three $newline$newline"
[3   spaces]
[]
[with 'quotes']
[double "quote"]
[
 line two 
 line three 

]
  • With a heads-up from @KamilCuk, I have made modifications to fix the issue of trailing newlines being trimmed.

It seems okay. But the concern is, if '$(printf "%s" "$arg" | sed "s/'/'\\\\''/g")' escaping only the single quote safe enough?

I try to look into how getopt does the escape.

https://github.com/util-linux/util-linux/blob/9abd5e4b99fb2d78d8dbbb9144aef68972c65e83/misc-utils/getopt.c#L133-L172

It seems that getopt only escape the single quote also, except when --shell tcsh.

1

There are 1 answers

8
tripleee On

There is no sane way to manipulate the global value of $@ from inside a function. My suggestion would be to refactor your code to avoid the repetitions by other means.

Perhaps like this:

#!/bin/sh

what=""
case "$1" in
  ( *one*   ) echo 1; what='more' ;;
  ( *two*   ) echo 2; what='yet more' ;;
  ( *three* ) echo 3; what='still more' ;;
  ( other ) what='other' ;;
  ( * ) ;;
esac
shift
[ "$what" ] && echo "$what"

printf '[%s]\n' "$@"