4
\$\begingroup\$

I'm trying to write a bash function that will take a command such as xcode-select -v or brew -v that outputs a lot of extra text, and format it to my liking. Just to further the example, say I run xcode-select -v, it will output xcode-select version 2333.. I want to be able to split this string up and only take the 2333 part so I can put it in an echo statement. I want the same function to be able to handle the various outputs of stuff like brew -v, or git --version, etc.

get_version_number() {
    # set local variable to executed arguments that is passed in
    local command="$($@)"
    # set up temp variable and assign to global IFS variable
    OIFS=$IFS
    # set up IFS to split the string on empty space
    IFS=" "
    # read commands output into an array and store it in $array variable
    read -a array <<< "$command"
    # clear out IFS back to what it original was for further use
    IFS=$OIFS
    # echo out array at a particular indices that should be passed in 
    echo ${array[@]}
}

# store output of this command into variable for further usage
version=$(get_version_number xcode-select -v)
echo $version

Edit 1: Originally the idea came from me having to copy/paste these same lines within different conditional blocks...

command=$(git -v)
OIFS=$IFS
IFS=" "
read -a output_array <<< "$command"
IFS=$OIFS
printf "Git version ${output_array[2]} is installed."

command=$(brew -v)
OIFS=$IFS
IFS=" "
read -a output_array <<< "$command"
IFS=$OIFS
printf "Homebrew version ${output_array[1]} is installed."
\$\endgroup\$
  • \$\begingroup\$ The "How do I do X" aspect of the question is off-topic for Code Review (see help center), though @rolfl has kindly answered it for you. In the future, try unix.stackexchange.com, superuser.com, or stackoverflow.com first for such questions, then let us review working code here. \$\endgroup\$ – 200_success Jan 31 '14 at 6:41
  • \$\begingroup\$ Sorry @200_success I did have working code, but I guess by posing it as a question, it leads to some confusion. Thanks for the kindness. \$\endgroup\$ – iamnewton Jan 31 '14 at 16:05
3
\$\begingroup\$

The trick for splitting up the version blurb for just the version number, is to pass the values you want in before the command, and then shift them off $@.

you have done a relatively unfamiliar-to-me parsing mechanism with the resetting the field separators, etc. Using bash as a parser is a problem for me (and I've never hated myself enough to try....).

I would set up a function that takes a co-ordinate of a version as a line/word combination, and rely on out-of-bash tools to do it......

I will probably get nailed for starting 4 sub-processes, but head, tail, and cut are small.... right? (Forgive me already, you won't be calling this in a tight loop, will you? You're already doing that a bit with the bash $( ...) operator...)

get_version_number() {
    local line=$1
    shift
    local word=$1
    shift

    # echo Line $line Word $word and Commend $@

    # set local variable to executed arguments that is passed in
    echo "$( $@ | head -$line | tail -1 | cut -d ' ' -f $word)"
}

Then you would call it like:

gitver=$(get_version_number 1 3 git --version)
perlver=$(get_version_number 2 4 perl -version)

echo Git and Perl are $gitver and $perlver respectively

which for me, produces:

Git and Perl are 1.7.1 and v5.10.1 respectively

This also allows you to do things like get the redhat version as

rhelver=$(get_version_number 1 7 cat /etc/issue)
javaver=$(get_version_number 1 3 java -version)

and even things like the current time 23:45:08 ;-)

time=$(get_version_number 1 4 date)

Some notes:

  • historically, it is 'expensive' to start lots of child processes. It still is a problem, but not quite as bad as it was. Doing that hundreds or thousands of times may add up to seconds of time wasted. If you can avoid creating child processes, do it.... but, sometimes it's just easier to be lazy than to be super-bash-smart.
  • I create an additional 4 processes (actually 5) each time you call the function. One for the $( ... ), one for the $@ inside that, one for head, one for tail, and one for cut. That is almost excessive.... but if you are calling this 100 times, then it's OK, if you are calling it 1000 times, you will be able to save seconds by doing it differently.
  • the $( ... ) operator is formally called the Command Substitution. What it means is that everything inside the structure is run as if in a 'child' bash process.... (so a new process is created, and the output is substituted in place of the command)
  • what the shift operator does is take $@ and remove the first item.... so, your function called as get_version_number 1 3 git --version gets 4 parameters, 1, 3, git and --version. We copy $1 off as $line, and then shift $@, so there are now only 3 parameters in $@. We then copy $1 again (but because of the shift, it is a different value) as $word, and then shift again to remove it. We are left with just git and --version as the parameters.

If you wanted, for example, to save one process each time, you could instead do:

get_version_number() {
    local line=$1
    shift
    local word=$1
    shift
    GOTVERSION="$( $@ | head -$line | tail -1 | cut -d ' ' -f $word)"
}

get_version_number 1 3 java -version
javaversion=$GOTVERSION

That saves using the $( .... ) and echo combination, so you save a process, but the code is a bit more complicated.


Edit 2.

'Obviously', what I suggest you do is not necessarily the best thing. Your code was working fine, and the mechanism will be faster (slightly) than mine because it does not do the additional 4 processes and keeps the logic inside bash internal commands.

Here is my 'shift' system applied to your code:

get_version_number() {
    local word=$1
    shift
    # set local variable to executed arguments that is passed in
    local command="$($@)"
    # set up temp variable and assign to global IFS variable
    OIFS=$IFS
    # set up IFS to split the string on empty space
    IFS=" "
    # read commands output into an array and store it in $array variable
    read -a array <<< "$command"
    # clear out IFS back to what it original was for further use
    IFS=$OIFS
    # echo out array at a particular indices that should be passed in 
    echo ${array[$word]}
}

and you would call it with the word-number for the version as the first argument:

gitver=$(get_version_number 3 git --version)

But this does not allow you to find versions on multi-line outputs.....

\$\endgroup\$
  • \$\begingroup\$ I'm a bit of a Bash noob, so would you mind breaking down the solution so I can understand it and/or can you point to some online resources. Particularly the 4 sub-processes part, the tight loop part, $(...) operator in my understand is only to call a function so it can be return/assigned to a value. The Perl part returns 5, on Mac so I'm curious as to if this isn't as full proof as I'd like. \$\endgroup\$ – iamnewton Jan 31 '14 at 4:46
  • 1
    \$\begingroup\$ Updated answer with some more detail.... zzzzzz time. \$\endgroup\$ – rolfl Jan 31 '14 at 5:07
  • \$\begingroup\$ This is even better than my dreamed up solution, so marking this one as the answer. Thanks @rolfl \$\endgroup\$ – iamnewton Jan 31 '14 at 18:50
1
\$\begingroup\$

To temporarily override a variable while executing a command, use this syntax1:

VAR=value command

That relieves you of the duty to save and restore $IFS.

get_version_number() {
    # set local variable to executed arguments that is passed in
    local command="$($@)"
    # set up IFS to split the string on empty space
    IFS=" " read -a array <<< "$command" 
    # echo out array at a particular indices that should be passed in  
    echo ${array[@]}
}

I'm not happy about the readability of $($@), nor do I like the idea of storing the entire command output in a variable when you only want the first line. An alternate approach is to pipe the command output to a subshell. (Forking a Bash subshell should be cheap, relative to forking and execing a command such as awk. Anyway, shell programs routinely use external commands, and performance is generally not a concern until it proves to be a problem.)

get_version_number() {
    $@ | while IFS=" " read -a array ; do
        echo ${array[@]}
        break     # Exit after processing the first line
    done
}

By the way, git -v is an error. You want git --version.


1 From bash(1):

The environment for any simple command or function may be augmented temporarily by prefixing it with parameter assignments, as described in Shell Parameters. These assignment statements affect only the environment seen by that command.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.