yolenoyer
yolenoyer

Reputation: 9445

Split bash command parameters in a smart and clean way

My final goal is to iterate in a clean way over all the parameters used in the last command line in bash, in order to find any path to a directory. Example of what I would wish:

$ cp some_file.txt /some/existing/folder; touch a_new_file.txt
$ my_find_func
Found "/some/existing/folder" in the last command.

My problem is about splitting the last command in a right way, in order to handle all possible cases. Now i'm using something like this:

function myfunc()
{
    last_com="$(history|tail -n2|head -n1|sed -n 's/^\s*[0-9]*\s*//p')"
    eval "args=( $last_com )"
    # Note: I don't care about the security issue that eval can cause

    for arg in "${args[@]}"; do
        echo "$arg"
    done
}

I like the simplicity of using eval in this way because it handles automatically quoted parameters, escaped spaces, glob expansion, etc... So I don't have to handle this by myself with a complicated awk or sed command.

It works fine with single commands, like this:

/afac/soq $ cd ..
/afac $ myfunc 
cd
..
/afac $ touch "some file.txt"
/afac $ myfunc 
touch
some file.txt

But obviously (because of ';' inside the array definition), it fails when I use multiple commands in a single line:

$ touch a_file; rm a_file
$ myfunc
bash: syntax error near unexpected token ';'
$ touch a_file && rm a_file
$ myfunc
bash: syntax error near unexpected token '&&'

So to make it work, I would to have to split the command string into parts when ;, && or || is encountered, without forgetting the case when these tokens are escaped or quoted, then saying goodbye to simplicity... I don't even know if I'm able yet to parse this in a good way, with the current knowledge I have about sed and awk...

What would be the most clean (and easiest) solution to get all the parameters of a command into an array, handling the possibility of quoted parameters, escaped characters, and multiple commands on a single-line?

It's maybe quite duplicate, but I didn't find any real solution anywhere.

Upvotes: 1

Views: 128

Answers (2)

Eugeniu Rosca
Eugeniu Rosca

Reputation: 5315

Right now I am only able to produce something like this:

#!/bin/bash

last_cmd='echo 1 2 "3 4" &  && echo "A B" C || echo D "E F" &  '

# Convert any of: &, && or || to semicolon
cmd=$(sed -e 's/&\?[ ]*&&/;/g' -e 's/&\?[ ]*||/;/g' -e 's/&[ ]*$//' <<< "$last_cmd")
# TODO: get rid of/convert any other symbols creating issues to eval
echo "processed cmd: $cmd"

# split the command string using semicolon as delimiter
IFS=';'
cmd_arr=($cmd)
IFS=' '
args=()
for onecmd in "${cmd_arr[@]}"; do
    eval "args+=($onecmd)"
done

for arg in "${args[@]}"; do
    echo "$arg"
done

Version 2

last_cmd='echo 1 2 "3 4"  && echo "A B" C || echo D ["E F"] $!%#^* '

# Remove ALL special characters not having a chance to appear in a pathname
cmd=$(sed 's/[][|*^#+&$!%]//g' <<< "$last_cmd")
echo "processed cmd: $cmd"

IFS=' ' eval "args=($cmd)"
for arg in "${args[@]}"; do
    echo "$arg"
done

Upvotes: 1

meuh
meuh

Reputation: 12255

You can do a bit better, though not all cases, with:

function myfunc(){
 set -- $(history 2 | sed 's/[ 0-9]*//;1q')
 for arg
 do echo "$arg"
 done
}

Upvotes: 0

Related Questions