Space separated argument to script - bash

I'm trying to use a wrapper bash script to execute some command with one parameter separated by space 'A B'.
foo-wrapper.sh content:
#/!bin/bash
foo $1
When running foo-wrapper.sh:
$bash -x foo-wrapper.sh "'A B'"
+ Error: foo ''\''A' 'B'\'''
The expected call would be: foo 'A B'
Any ideas how to make that work?

In the wrapper replace $1 by "$1".
If your arguments/variables contain space characters, you need to quote them to prevent them from being split.
In the call instead of "'A B'" use one of "A B", 'A B' or A\ B.
These are the most usual ways of specifying strings in BASH. (Note that the whitespace only makes a difference in the 3rd case. You would have to use quotes in the first two cases no matter whether you have a space or not.)
If you want to play around a little to get a feeling, you can start with:
$ foo() { echo "count: $#"; echo "arg 1: $1"; echo "arg 2: $2"; }
$ foo A\ B C
count: 2
arg 1: A B
arg 2: C

Related

Bash: parse shell command string

Given a shell command string (ls a\ b "c d" $f), can bash parse it into an array, preferrably as bash itself would see it after all expansions, right before execution (([0]=ls [1]='a b' [2]='c d' [3]=<value of $f>))? I.e. the read() part of a REPL.
I'd like to write a custom bind -x function, and I need to parse the current READLINE_LINE. I've tried read -a, but that only takes care of backslashes (e.g. "a b" gets split into <"a> and <b">)
I hope you can make use of the following code
#!/usr/bin/env bash
parse(){
declare -g cmd
local i
for i in $(seq 1 $#); do
cmd[$((i-1))]="${!i}"
done
declare -p cmd
}
touch "a b" "c d" "e"
f="e"
trap 'eval "parse $BASH_COMMAND"; trap -- DEBUG' DEBUG
ls a\ b "c d" $f
declare -p cmd

Use command expansion as argument of `for` loop or initializing an array

I met a problem with using a function's output (strings contain spaces) in for loop and array initialization.
I wonder why the expressions below has different behavior.
It performs right when normal strings passed in for in structure.
$ for str in 'a b' 'c d'; do echo $str; done
a b
c d
But when I use output of a function, things are different.
foo ()
{
echo "'a b' 'c d'"
}
$ for str in $(foo); do echo $str; done
'a
b'
'c
d'
Similar situation happens on initializing an array too:
$ strs=('a b' 'c d')
$ echo "${strs[#]}"
a b c d
$ echo "${strs[0]}"
a b
But
$ strs=( $(foo) )
$ echo "${strs[#]}"
'a b' 'c d'
$ echo "${strs[0]}"
'a
Use output of a function, which has same value of the constants, but produces different results. Why this would happen? What's the internal mechanism behind it.
Most important, how can I use output of a function gain same result of using constants.
Do have a look at the link in the comment of this question, it contains more than just this question, that helps very much.
A workaround (assuming none of your array elements can contain newlines) is to simply output unquoted lines of text, and use readarray to populate your array.
foo () {
echo 'a b'
echo 'c d'
}
readarray -t strs < <(foo)
for str in "${strs[#]}"; do
echo "$str"
done
this can be done with a while loop without storing command output in an array
foo() {
echo 'a b'
echo 'c d'
}
foo | while read; do
echo "$REPLY"
done
tested on both bash 4.4.23 and adb shell

Pass parameters that contain whitespaces via shell variable

I've got a program that I want to call by passing parameters from a shell variable. Throughout this question, I am going to assume that it is given by
#!/bin/sh
echo $#
i.e. that it prints out the number of arguments that are passed to it. Let's call it count-args.
I call my program like this:
X="arg1 arg2"
count-args $X
This works quite well. But now one of my arguments has a whitespace in it and I can't find a way to escape it, e.g. the following things do not work:
X="Hello\ World"
X="Hello\\ World"
X="'Hello World'"
In all of the cases, my program count-args prints out 2. I want to find a way so I can pass the string Hello World and that it returns 1 instead. How?
Just for clarification: I do not want to pass all parameters as a single string, e.g.
X="Hello World"
count-args $X
should print out 2. I want a way to pass parameters that contain whitespaces.
Use an array to store multiple, space-containing arguments.
$ args=("first one" "second one")
$ count-args "${args[#]}"
2
This can be solved with xargs. By replacing
count-args $X
with
echo $X | xargs count-args
I can use backslashes to escape whitespaces in $X, e.g.
X="Hello\\ World"
echo $X | xargs count-args
prints out 1 and
X="Hello World"
echo $X | xargs count-args
prints out 2.
count-args "$X"
The quotes ensure in bash, that the whole content of variable X is passed as a single parameter.
Your Counting script:
$ cat ./params.sh
#!/bin/sh
echo $#
For completeness here is what happens with various arguments:
$ ./params.sh
0
$ ./params.sh 1 2
2
$ ./params.sh
0
$ ./params.sh 1
1
$ ./params.sh 1 2
2
$ ./params.sh "1 2"
1
And here is what you get with variables:
$ XYZ="1 2" sh -c './params.sh $XYZ'
2
$ XYZ="1 2" sh -c './params.sh "$XYZ"'
1
Taking this a bit further:
$ cat params-printer.sh
#!/bin/sh
echo "Count: $#"
echo "1 : '$1'"
echo "2 : '$2'"
We get:
$ XYZ="1 2" sh -c './params-printer.sh "$XYZ"'
Count: 1
1 : '1 2'
2 : ''
This looks like what you wanted to do.
Now: If you have a script you cannot control and neither can you control the way the script is invoked. Then there is very little you can do to prevent a variable with spaces turning into multiple arguments.
There are quite a few questions around this on StackOverflow which indicate that you need the ability to control how the command is invoked else there is little you can do.
Passing arguments with spaces between (bash) script
Passing a string with spaces as a function argument in bash
Passing arguments to a command in Bash script with spaces
And wow! this has been asked so many times before:
How to pass argument with spaces to a shell script function

Remove last word in bash variable

I have something like that:
...
args=$*
echo $args
...
result is
unusable1 unusable2 useful useful ... useful unusable3
I need remove all "unusable" args. They always at first, second and last position.
After some investigation i find ${*:3} bash syntax. It help remove first two.
...
args=${*:3}
echo $args
...
result is
useful useful ... useful unusable3
But I can't find how to remove last word using same nice syntax.
You can use a function/script like this to print all but last arguments:
func() {
echo "${#:1:$#-1}";
}
func aa bb cc dd ee
aa bb cc dd
func foo bar baz hello how are you
foo bar baz hello how are
args=${*:3} flattens your argument list. You don't want to do that. Consider following the pattern given below instead:
# this next line sets "$#" for testing purposes; you don't need it in real life
set -- \
"first argument" \
"second argument" \
"third argument" \
"fourth argument" \
"fifth argument"
# trim the first two
args=( "${#:2}" )
# trim the last one
args=( "${args[#]:1:$(( ${#args[#]} - 2 ))}" )
# demonstrate the output content
printf '<%s>\n' "${args[#]}"
Running the above yields the following output:
<third argument>
<fourth argument>
...and, by doing so, demonstrates that it's correctly keeping arguments together, even when they contain spaces or wildcard characters.
For a shell completion script, you might also consider:
printf '%q ' "${args[#]}"
...which quotes content in such a way as to be eval'able by the shell.
Using your syntax, you can use this :
args=${*:3:$#-3}
Explanation :
${*:offset:length}
offset is 3 to begin to the third argument and length is the number of arguments minus 3 (two first and last one).
You could use awk:
args="unusable1 unusable2 useful useful ... useful unusable3"
args=$(awk '{$1=$2=$NF="";print}' <<< "$args")
echo "$args"
Output:
useful useful ... useful
The command sets the the first, second and the last ($NF) position to an empty string. NF holds the number of fields in awk. Therefore $NF is the last column.
bash doesn't really provide such filtering methods for arrays. The best option is to just use a loop and filter one at a time.
for arg; do # Implicitly iterate over $#
[[ $arg =~ unusable ]] && continue
args+=( "$arg" )
done
Try with this:
args=$*
useful=${args#* }
useful=${useful#* }
useful=${useful% *}
Then you wlill get the interesting parameters in the $result variable.

Preserve argument splitting when storing command with whitespaces in variable

I'd like to store a command line in a variable, and then execute that command line. The problem is, the command line has arguments with spaces in them. If I do
$ x='command "complex argument"'
$ $x
it calls command with "complex and argument". I tried using "$x" thinking it would preserve the argument splittings, but it only tries to execute a program with the file name command "complex argument". I also tried variations of the quotes (' vs ") and using exec, but it didn't help. Any ideas?
Edit: eval "$x" almost works, but if the whitespaces separating arguments are newlines and not spaces, then it treats the lines as separate commands.
Edit2: The extra " quotes were too much, and made eval interpret the newlines not as spaces, but as command delimiters. The solutions in the answers all work.
For testing purposes, I define:
$ function args() { while [[ "$1" != "" ]]; do echo arg: $1; shift; done }
This works as expected:
$ args "1 2" 3
arg: 1 2
arg: 3
$ x="arg 4 5 6"
$ $x
arg: 4
arg: 5
arg: 6
This doesnt:
$ x="args \"3 4\" 5"
$ $x
arg: "3
arg: 4"
arg: 5
A safe approach is to store your command line in a BASH array:
arr=( command "complex argument" )
Then execute it:
"${arr[#]}"
OR else another approach is to use BASH function:
cmdfunc() {
command "complex argument"
}
cmdfunc
Another solution is
x='command "complex argument"'
eval $x

Resources