Filetype backup script
As you say, the requirements are, let's say, unconventional.  But in some ways, that's good preparation for crazy requirements you'll get in working life!  Except that as a student, you might not get to ask the &quot;five whys&quot; to determine whether the requirements are actually grounded, and to get them changed.
There are still some clarifications of the requirements:

Should only regular files be archived, or should we include directories and devices, too?
Should the archive contain full pathnames of the contained files, or relative names, or just basenames?  If only basenames, then how should we deal with conflicts?
Should the destination directory be created if it doesn't already exist?

As this is a homework assignment, you may not be in a position to ask these questions - in that case, you should assume a position and state it clearly in your answer.  If possible, localise the effects of a wrong assumption, to make it easy to change when you get more information (again, that's a good reflection of working life).

Working through the code from the beginning:
list_begin=1 # where the filetype arguments begins
list_end=$ # where the filetype arguments end

target_directory=$(pwd)
destination_directory=$(pwd)

It looks like you can save two invocations of pwd by using . as the default directory name - you never need an absolute path.
A more common approach is to remove option arguments using shift as they are parsed; then all of $* are your inputs.  I'm assuming $  there is a typo for $#?
Here's the outline:
while test -n &quot;${1+:}&quot;
do
    case &quot;$1&quot; in
        -t|--target)
            target=&quot;$2&quot;
            shift 2
            ;;
        -d|--destination)
            destination=&quot;$2&quot;
            shift 2
            ;;
        *)
            process_file &quot;$1&quot;
            shift
            ;;
    esac
done

I've included long-name alternatives just to show how it's done, not because that's a requirement.  This approach allows the user to specify -t and -d more than once, to change those settings during a run.  It's not clear whether you are required to enforce the ordering constraints you were given, or if that's just limiting the problem space to make it easier.  If the former, you can just process them in turn:
if [ &quot;-t&quot; = &quot;$1&quot; ]
then
    target=&quot;$2&quot;
    shift 2
fi
if [ &quot;-d&quot; = &quot;$1&quot; ]
then
    destination=&quot;$2&quot;
    shift 2
fi

and so on.
I've also fixed the quoting above - you'll find that if you miss it out, then people will want at target or destination with whitespace in the name!

# confirm directories are real
if [[ ! -d &quot;${target_directory}&quot; ]]; then # invalid -t
    echo &quot;backup: \&quot;${target_directory}\&quot; is not valid&quot;
    exit
elif [[ ! -d &quot;${destination_directory}&quot; ]]; then # invalid -d
    echo &quot;backup: \&quot;${destination_directory}\&quot; is not valid&quot;
    exit
fi

You don't need to use Bash [[ here - it's more portable to stick to plain old [ (aka test).  You'll want your error messages to go to the error stream (&gt;&amp;2), and you'll want to exit with a non-zero value, so that anything executing your program will know it didn't succeed.  I like to define a small function to do that:
die() {
    echo &quot;$@&quot; &gt;&amp;2
    exit 1
}

Then I can write:
test -d &quot;$target_directory&quot; || die &quot;$0: $target_directory is not a directory&quot;
test -r &quot;$target_directory&quot; &amp;&amp; test -x &quot;$target_directory&quot; || die &quot;$0: $target_directory cannot be read&quot;
test -d &quot;$destination_directory&quot; || die &quot;$0: $destination_directory is not a directory&quot;
test -w &quot;$destination_directory&quot; || die &quot;$0: $destination_directory is not writable&quot;


declare -a filetypes # array to store filetypes

for ((i=${list_begin}, j=0; i&lt;=${list_end}; i++, j++)); do # fetch all filetype arguments and add to array
    eval dir=\${$i}
    filetypes[${j}]=${dir}
done

I don't think it's necessary to do all this copying.  You can simply process each argument as you come to it.

for i in &quot;${filetypes[@]}&quot;; do # for each filetype
    f_list=&quot;${target_directory}/*.${i}&quot; # directory to iterate through

You shouldn't be quoting the * there, because you want the shell to expand it.  If you expand it later, you may well end up splitting $i.

My version
#!/bin/sh
set -e

die() {
    echo &quot;$@&quot; &gt;&amp;2
    exit 1
}

process_suffix() {
    for file in &quot;${source-.}&quot;/*.&quot;$1&quot;
    do
        # Warning: may overwrite existing archive
        test -e &quot;$file&quot; \
             &amp;&amp; tar cfz &quot;${destination:-.}/${file##*/}.tar.gz&quot; -C &quot;${source-.}&quot; -- &quot;${file#${source-.}/}&quot;
    done
    echo &quot;$0: finished *.$1 files&quot;
}

while test -n &quot;${1+:}&quot;
do
    case &quot;$1&quot; in
        -t|--source)
            test -d &quot;$2&quot; || die &quot;$0: $2 is not a directory&quot;
            test -r &quot;$2&quot; &amp;&amp; test -x &quot;$2&quot; || die &quot;$0: $2 cannot be read&quot;
            source=&quot;$2&quot;
            shift 2
            ;;
        -d|--destination)
            test -d &quot;$2&quot; || die &quot;$0: $2 is not a directory&quot;
            test -w &quot;$2&quot; || die &quot;$0: $2 is not writable&quot;
            destination=&quot;$2&quot;
            shift 2
            ;;
        *)
            process_suffix &quot;$1&quot;
            shift
            ;;
    esac
done

echo &quot;$0: files saved to $destination&quot;

Additional notes:

Everything is now POSIX shell, so we can use /bin/sh which may be faster, more available, etc (bash, ksh, dash etc. are all sufficiently POSIX-compliant).
I've given -- to tar to indicate that subsequent arguments are filenames, even if they begin with -.
I default uninitialised source and destination directories - see the Parameter Expansion section of the shell man page.
Archive filenames are all relative to the source directory.
I use &quot;$0&quot; in messages to show the actual name the program was invoked as.
Test cases should include file and directory names containing spaces or beginning with - (that's a good general rule for testing shell scripts).
Remember that (by default) a non-matching glob is treated literally.  So if there are no matching files, we'll want to ignore *.suffix when archiving (but still archive a file that really does have * in the name).