3

Context:

I have an old bash script with a big section parsing its arguments. It happens now that I need to call this section twice, so I plan to move it to a function to avoid code duplication.

The problem:

In that section, set --, shift and $@ are used, meaning that they won't apply to the script anymore, but to the function, which is wrong.

Question:

From within the function, is there any way to get and set the script arguments ?

Scheme:

Something like this:

#!/bin/bash
# > 5000 lines

process_arg()
{
   # about 650 lines

   # set --
   # $@ $* $1 ...
   # shift <n>
}

while (( $# > 0 )); do
   case $1 in
      <cond>)
         <some code here>
         process_arg
         <some more code here>

      <other conditions and code here>

      *)
         <some different code here>
         process_arg
         <some different more code here>
   esac
   shift 1
done
Jacques
  • 551
  • 1
    Why do you have to operate on the script arguments? Could you not use another array with a copy of them? –  Dec 17 '19 at 14:34
  • 1
    Call your function with the arguments of the script: myfunc "$@". – Kusalananda Dec 17 '19 at 14:44
  • 1
    @Tomasz. Because this script is old, is about 5000 lins long, and has at least more than 100 set --, $@ and shift combine. I would like better to avoid having to refactor it completely, if possible, or to do it the lightest way possible if I have too. You know: ideal world vs reality; – Jacques Dec 17 '19 at 14:46
  • @Kusalananda. It's OK to get the params, but not to set them. – Jacques Dec 17 '19 at 14:46
  • @steeldriver: maybe half of my question but by far less than half of the problem ;-) – Jacques Dec 17 '19 at 14:47
  • @Kusalananda is right. You need to go the myfunc "$@" route. Or you could move the section into a configuration file and use a function to read configuration files. Or just make a copy of your script and have two scripts doing nearly the same thing. Remember that bash variables are global by default, so setting things from within the function is easy. – markgraf Dec 17 '19 at 15:01
  • @markgraf, and how do you change the global $@ ? To be more detailed, I have no other option than to keep part of the argument parsing in the function, and part outside. There will be set --, $@ and shift inside and outside. Also, code duplication is not an option, the script in 5000 lines long and the function would be 650 lines long. – Jacques Dec 17 '19 at 15:05
  • @Jacques Pass the name of an array, and receive it as a name reference variable (declare -n), then set the elements of that array in the function. The main code can then call the function with arrayname "$@" as arguments and then, if it wants to, use set -- "${arrayname[@]}". I'm sure there is a similar question somewhere on the site. – Kusalananda Dec 17 '19 at 15:07
  • You do not need to. You parse $@ inside your function, extract whatever you need and set the rest yourself. Make two functions and let them have their own copy of args local my_func_args="$@". Do whatever you want with $my_func_args. – markgraf Dec 17 '19 at 15:09
  • @markgraf, argument processing would be mixed inside and outside the function. Not that simple. Arguments at global level would be impacted by the function, and then processed at global level too. It is not going into the function once and going out, it is the function being called in the argument parsing loop, and impacting that loop as well. – Jacques Dec 17 '19 at 15:24
  • The only solution I see so far is to store $@ in a global array, then: use wrapper function for set --, and shift like functionalities. something like arg_shift(), arg_set(). Lightest refactoring I see (mostly search/replace in script). – Jacques Dec 17 '19 at 15:34
  • Send parameters to the function: myfunc "$@". Set parameters from function: set argument list in array param inside the function and set -- "${param[@]}" if the shell has arrays. Or: set -f; set -- $(myfunc "$@") if the arguments could be given as output of the function and they have no spaces or newlines (split chars in IFS). –  Dec 17 '19 at 15:49

1 Answers1

0

Disclaimer:

From the discussion above, I implemented a solution. This is not, by far, the one I dreamed of, because of the verbosity of ${args_array[1]}, compared to $1. Makes the source less readable. So improvements, or a better solution are still welcome.

Source:

Tested, something like this:

#!/bin/bash 

#########################    
# DEBUG
#########################    

# set -x
PS4='${xchars:-+} ${BASH_SOURCE}:${LINENO} (${FUNCNAME[@]}) + ' # full stack

#########################    
# INITIAL ARGS FOR TEST
#########################    
set -- a b c d e f g h

#########################    
# UTILITIES
#########################    

args_array=( "$@" ) # script args here

args_shift() # Usage readability OK, replaces shift <n>
{
   typeset n=${1:-1}

               echo "args_shift $1 in ${FUNCNAME[1]} -- ${args_array[@]}"

   args_array=( "${args_array[@]:$n}" ) # ${1:-1} unsupported in this context

               echo "args_shift $1 in ${FUNCNAME[1]} ++ ${args_array[@]}"
}

args_set() # Usage readability OK, replaces set -- <args>
{
               echo "args_set $@ in ${FUNCNAME[1]} -- ${args_array[@]}"

   args_array=( "$@" ) # function args here

               echo "args_set $@ in ${FUNCNAME[1]} ++ ${args_array[@]}"
}

# Usage
# search/replace OK, and good readability afterward
# shift <n> ---> args_shift <n>
# set -- <args> ---> args_set <args>

# search/replace OK, but bad readability afterward, and refactoring--
# $@ ---> ${args_array[@]}
# $# ---> ${#args_array[@]}
# $1 ---> ${args_array[0]}   !!! 1 -> 0
# $2 ---> ${args_array[1]}   !!! 2 -> 1
# etc

#########################
# TEST
#########################    

f()
{
   args_shift
}

g()
{
   args_set A B C D
}

# main

echo "main -- ${args_array[@]}"
f
args_shift 2
f
g
args_shift
f
echo "main ++ ${args_array[@]}"

Output:

main -- a b c d e f g h
args_shift  in f -- a b c d e f g h
args_shift  in f ++ b c d e f g h
args_shift 2 in main -- b c d e f g h
args_shift 2 in main ++ d e f g h
args_shift  in f -- d e f g h
args_shift  in f ++ e f g h
args_set A B C D in g -- e f g h
args_set A B C D in g ++ A B C D
args_shift  in main -- A B C D
args_shift  in main ++ B C D
args_shift  in f -- B C D
args_shift  in f ++ C D
main ++ C D

Remarks:

  1. Works but not the most readable solution, and refactoring not so light, because there are several forms of usage to take into consideration: $1, more or less ${1[:/][^}]} or ${!1[:/][^}]} etc, while avoiding those in function, awk, perl etc.
  2. For some, as variable names are case sensitive in bash and, I think, more or less seldom used, on could use A or _A instead of args_array but, to my taste, ${A[1]} or so is even less readable in a long source than ${args_array[1]}.

My situation:

There are at least 616 occurrences to take care of... carefully (some are in functions, awk or perl scripts etc)

for s in shift 'set --' '$@' '${@' '$*' '${*' '$1' '${1' '$2' '${2' '$3' '${3' '$4' '${4' '$5' '${5'; do
   printf '%-10s: %d\n' "$s " "$(fgrep $s <script>|wc -l)"
done # |awk '{sum+=$NF};END{print sum}'

shift     : 44
set --    : 189
$@        : 39
${@       : 2
$*        : 7
${*       : 0
$1        : 182
${1       : 79
$2        : 48
${2       : 3
$3        : 15
${3       : 0
$4        : 8
${4       : 0
$5        : 0
${5       : 0
Jacques
  • 551