Search code examples
bashparametersarguments

Changing the value of an argument and using options in bash


I'm writing a bash script to automate some job, and in the meantime practice with bash. I'm writing a script to automate some git cloning and updating stuff. My script will have three options (-g, -f, -h). When someone types -h it should display the help message (which I have written but omitted it below), when someone writes -g, it should accept at least one argument, but it can also accept second one as optional. From which, the first one will be the url of the repository to clone and second one will be the directory, where to clone it. On the other hand, if someone types -f, the script should get just one argument, just a file name. Then I want to read the file, line by line, and do git cloning and updating for each git url inside the file.

If I run the script I get the following error message for each option, and if I call it without any option, or with a valid option followed by some other argument, it still doesn't do anything, just returns:

./gitupdate.sh: option requires an argument -- g

I guess it doesn't use $2 and $3 somehow in my code. But then again, when I pass -h, all it has to do is call the help function and display the message, doesn't need to use any other argument.

I guess the problem is because I have something wrong at the bottom, where I use getopts to get the option name specified by the user. In my code I assume that the option is the first argument, $1, and then the second one $2 is the url or filename, according to the option specified, and the $3 is the optional one that works only with -g option.

Below you can find my code:

#!/bin/bash

declare default_directory=$HOME
declare action
declare src

function clone_repo() {
    if [ -n "$2" ]; then
        if [ "$(ls -A "$2" 2>/dev/null)" ]; then
            cd "$2"
        else
            git clone "$1" "$2"
        fi
    else
        git clone "$1"
        #TODO: Get the directory name created by cloning
                #      and cd to it.
    fi

    git remote add upstream "$1"
    git fetch upstream
}

function read_repos_from_file() {
    if [ -f "$1" ]; then
        while read -r line; do
            clone_repo "$line" "$2"
        done < "$1"
    else
        echo -e "Error: The specified file could not be found."
        exit 1
    fi
}

while getopts "f:h:r" option
do
    case "${option}" in
        f) action=read_repos_from_file; src="$OPTARG";;
        g) action=clone_repo; src="$OPTARG";;
        h) help ; exit 1 ;;
    esac
done

shift $((OPTIND-1))

[ -z "$action" ] && ( help; exit 1 )

If someone could help me, I would be glad.

EDIT: Corrected some typo mistakes in the above code. And updated the error message. The script doesn't work correctly. I guess I need to change something in my code, to make it do something, and actually get $2 and $3 arguments. It even doesn't display the help message, when -h option is passed, and all I do there is call the help function that I have previously created. Maybe I need to somehow change my getopts part.

EDIT 2: Made the advised changes, and changed the above code.


Solution

  • git() is the beginning of a function definition (the keyword function is optional when the function name is followed by parentheses). If you want to call a function git() you need to define it first, and call it without the parentheses:

    function git() {
      # do stuff
    }
    
    git
    

    It's not good practice to create functions with the same name as existing binaries, though. In your case you should probably just call git clone with the line read from the file:

    while read -r line; do
      git clone "$line"
    done < "${file}"
    

    Edit: Updated, since the question changed significantly.

    Your argument processing is … weird, to be frank. When you're using an option parser, you shouldn't work around the way that option parser works. "g:" means an option -g with exactly one argument. Don't try to make it an option with more than one argument, one of them being optional on top of it. If you need an additional (optional) argument for an output directory, make that either another option (e.g. "d:") or a non-option argument.

    I'd suggest to change your option-processing to something like this:

    while getopts "f:g:h" option; do
      case "$option" in
        f) action=file; src="$OPTARG";;
        g) action=repo; src="$OPTARG";;
        h) help; exit 1;;
      esac
    done
    
    shift $((OPTIND-1))
    
    [ -z "$action" ] && ( help; exit 1 )
    

    After this "$@" holds only non-option arguments (in this case the optional output directory), so you could call your functions like this:

    $action $src "$@"
    

    with the functions simplified to something like this:

    function repo() {
      if [ -n "$2" ]; then
        if [ "$(ls -A "$2" 2>/dev/null)" ]; then
          cd "$2"
        else
          git clone "$1" "$2"
        fi
      else
        git clone "$1"
      fi
      ...
    }
    
    function file() {
      if [ -f "$1" ]; then
        while read -r line; do
          repo "$line" "$2"
        done < "$1"
      else
        echo "Error: The specified file could not be found."
        exit 1
      fi
    }
    

    On a more general note, you should make your names more self-explanatory. Better names for functions for cloning a repository or reading repositories from a file would be something like clone_repo and read_repos_from_file respectively. Also, -c or -r would be better mnemonics for an option that triggers the cloning of a repository.