Pass parameters that contain whitespaces via shell variable - bash

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

Related

Issue understanding a parameter expansion in a bash script

I am trying to understand what a parameter expansion does inside a bash script.
third_party_bash_script
#!/bin/sh
files="${*:--}"
# For my understanding I tried to print the contents of files
echo $files
pkill bb_stream
if [ "x$VERBOSE" != "" ]; then
ARGS=-v1
fi
while [ 1 ]; do cat $files; done | bb_stream $ARGS
When I run ./third_party_bash_script, all it prints is a hyphen - and nothing else. Since it did not make sense to me, I also tried to experiment with it in the terminal
$ set one="1" two="2" three="3"
$ files="${*:--}"
$ echo $files
one="1" two="2" three="3"
$ set four="4"
$ files="${*:--}"
four="4"
I can't seem to understand what it's doing. Could someone kindly help me with the interpretation of ${*:--} by the sh?
"$#" is an array of the arguments passed to your script, "$*" is a string of all of those arguments concatenated with blanks in between.
"${*:--}" is the string of arguments if any were provided (:-), or - otherwise which means "take input from stdin" otherwise.
"${#:--}" is the array of arguments if any were provided (:-), or - otherwise which means "take input from stdin" otherwise.
$ cat file
foo
bar
$ cat tst.sh
#!/usr/bin/env bash
awk '{ print FILENAME, $0 }' "${#:--}"
When an arg is provided to the script, "$#" contains "file" so that is the arg that awk is called with:
$ ./tst.sh file
file foo
file bar
When no arg is provided to the script, "$#" is empty so awk is called with - (meaning read from stdin) as it's arg:
$ cat file | ./tst.sh
- foo
- bar
You almost always want to use "${#:--}" rather than "${*:--}" in this context, see https://unix.stackexchange.com/questions/41571/what-is-the-difference-between-and for more info on "$#" vs "$*".
${param:-default} expands to the value of $param if $param is set and not empty, otherwise it expands to default.
$* is all the command-line arguments to the script.
In ${*:--}, param is * and default is -. If $* is not an empty string, it expands to the script arguments. If it's empty, it expands to the default value -.
This can be used in a script to implement the common behavior that a program reads from the files listed in its arguments, and reads from standard input if no filename arguments are given. Many commands treat an input filename argument - as standing for the standard input.
NOTE: addressing OP's original, pre-edited post ...
See shell parameter expansion for a brief review of different options.
While the other answers reference the use of ${*:--} (and ${#:--}) as a alternate means of reading from stdin, OP's sample script is a bit simpler ... if the variable $* (ie, script's command line args) is empty then replace with the literal string -.
We can see this with a few examples:
$ third_party_bash_script
-
$ third_party_bash_script a b c
a b c
$ echo 'a b c' | third_party_bash_script
-
If we replace ${*:--} with ${*:-REPLACEMENT}:
$ third_party_bash_script
REPLACEMENT
$ third_party_bash_script a b c
a b c
$ echo 'a b c' | third_party_bash_script
REPLACEMENT
I'm guessing in OP's actual script there's more going on with the $files variable so in order to know for sure how the ${*:--} is being processed we'd need to see the actual script and how it's referencing the $files variable.
As for OP's set|files=|echo code snippets:
$ set one="1" two="2" three="3"
$ files="${*:--}"
$ echo $files
one=1 two=2 three=3
We can see the same behavior from the script with:
$ third_party_bash_script one="1" two="2" three="3"
one=1 two=2 three=3

convert inputs arguments to string with spaces [duplicate]

In shell scripts, what is the difference between $# and $*?
Which one is the preferred way to get the script arguments?
Are there differences between the different shell interpreters about this?
From here:
$# behaves like $* except that when quoted the arguments are broken up properly if there are spaces in them.
Take this script for example (taken from the linked answer):
for var in "$#"
do
echo "$var"
done
Gives this:
$ sh test.sh 1 2 '3 4'
1
2
3 4
Now change "$#" to $*:
for var in $*
do
echo "$var"
done
And you get this:
$ sh test.sh 1 2 '3 4'
1
2
3
4
(Answer found by using Google)
A key difference from my POV is that "$#" preserves the original number
of arguments. It's the only form that does. For that reason it is
very handy for passing args around with the script.
For example, if file my_script contains:
#!/bin/bash
main()
{
echo 'MAIN sees ' $# ' args'
}
main $*
main $#
main "$*"
main "$#"
### end ###
and I run it like this:
my_script 'a b c' d e
I will get this output:
MAIN sees 5 args
MAIN sees 5 args
MAIN sees 1 args
MAIN sees 3 args
With $# each parameter is a quoted string. Otherwise it behaves the same.
See: http://tldp.org/LDP/abs/html/internalvariables.html#APPREF

Bash script - variable content as a command to run

I have a Perl script that gives me a defined list of random numbers that correspond to the lines of a file. Next I want to extract those lines from the file using sed.
#!/bin/bash
count=$(cat last_queries.txt | wc -l)
var=$(perl test.pl test2 $count)
The variable var returns an output like: cat last_queries.txt | sed -n '12p;500p;700p'. The problem is that I can't run this last command. I tried with $var, but the output is not correct (if I run manually the command it works fine, so no problem there). What is the correct way to do this?
P.S: Sure I could do all the work in Perl, but I'm trying to learn this way, because it could help me in other situations.
You just need to do:
#!/bin/bash
count=$(cat last_queries.txt | wc -l)
$(perl test.pl test2 $count)
However, if you want to call your Perl command later, and that's why you want to assign it to a variable, then:
#!/bin/bash
count=$(cat last_queries.txt | wc -l)
var="perl test.pl test2 $count" # You need double quotes to get your $count value substituted.
...stuff...
eval $var
As per Bash's help:
~$ help eval
eval: eval [arg ...]
Execute arguments as a shell command.
Combine ARGs into a single string, use the result as input to the shell,
and execute the resulting commands.
Exit Status:
Returns exit status of command or success if command is null.
You're are probably looking for eval $var.
line=$((${RANDOM} % $(wc -l < /etc/passwd)))
sed -n "${line}p" /etc/passwd
just with your file instead.
In this example I used the file /etc/password, using the special variable ${RANDOM} (about which I learned here), and the sed expression you had, only difference is that I am using double quotes instead of single to allow the variable expansion.
There're 2 basic ways of executing a string command in a shell script whether it's given as parameter or not here's.
COMMAND="ls -lah"
$(echo $COMMAND)
or
COMMAND="ls -lah"
bash -c $COMMAND
In the case where you have multiple variables containing the arguments for a command you're running, and not just a single string, you should not use eval directly, as it will fail in the following case:
function echo_arguments() {
echo "Argument 1: $1"
echo "Argument 2: $2"
echo "Argument 3: $3"
echo "Argument 4: $4"
}
# Note we are passing 3 arguments to `echo_arguments`, not 4
eval echo_arguments arg1 arg2 "Some arg"
Result:
Argument 1: arg1
Argument 2: arg2
Argument 3: Some
Argument 4: arg
Note that even though "Some arg" was passed as a single argument, eval read it as two.
Instead, you can just use the string as the command itself:
# The regular bash eval works by jamming all its arguments into a string then
# evaluating the string. This function treats its arguments as individual
# arguments to be passed to the command being run.
function eval_command() {
"$#";
}
Note the difference between the output of eval and the new eval_command function:
eval_command echo_arguments arg1 arg2 "Some arg"
Result:
Argument 1: arg1
Argument 2: arg2
Argument 3: Some arg
Argument 4:
Better ways to do it
Using a function:
# define it
myls() {
ls -l "/tmp/test/my dir"
}
# run it
myls
Using an array:
# define the array
mycmd=(ls -l "/tmp/test/my dir")
# run the command
"${mycmd[#]}"
cmd="ls -atr ${HOME} | tail -1" <br/>
echo "$cmd" <br/>
VAR_FIRST_FILE=$( eval "${cmd}" ) <br/>
or
cmd=("ls -atr ${HOME} | tail -1") <br/>
echo "$cmd" <br/>
VAR_FIRST_FILE=$( eval "${cmd[#]}" )

What is the difference between $# and $* in shell scripts?

In shell scripts, what is the difference between $# and $*?
Which one is the preferred way to get the script arguments?
Are there differences between the different shell interpreters about this?
From here:
$# behaves like $* except that when quoted the arguments are broken up properly if there are spaces in them.
Take this script for example (taken from the linked answer):
for var in "$#"
do
echo "$var"
done
Gives this:
$ sh test.sh 1 2 '3 4'
1
2
3 4
Now change "$#" to $*:
for var in $*
do
echo "$var"
done
And you get this:
$ sh test.sh 1 2 '3 4'
1
2
3
4
(Answer found by using Google)
A key difference from my POV is that "$#" preserves the original number
of arguments. It's the only form that does. For that reason it is
very handy for passing args around with the script.
For example, if file my_script contains:
#!/bin/bash
main()
{
echo 'MAIN sees ' $# ' args'
}
main $*
main $#
main "$*"
main "$#"
### end ###
and I run it like this:
my_script 'a b c' d e
I will get this output:
MAIN sees 5 args
MAIN sees 5 args
MAIN sees 1 args
MAIN sees 3 args
With $# each parameter is a quoted string. Otherwise it behaves the same.
See: http://tldp.org/LDP/abs/html/internalvariables.html#APPREF

Getting the last argument passed to a shell script

$1 is the first argument.
$# is all of them.
How can I find the last argument passed to a shell
script?
This is Bash-only:
echo "${#: -1}"
This is a bit of a hack:
for last; do true; done
echo $last
This one is also pretty portable (again, should work with bash, ksh and sh) and it doesn't shift the arguments, which could be nice.
It uses the fact that for implicitly loops over the arguments if you don't tell it what to loop over, and the fact that for loop variables aren't scoped: they keep the last value they were set to.
$ set quick brown fox jumps
$ echo ${*: -1:1} # last argument
jumps
$ echo ${*: -1} # or simply
jumps
$ echo ${*: -2:1} # next to last
fox
The space is necessary so that it doesn't get interpreted as a default value.
Note that this is bash-only.
The simplest answer for bash 3.0 or greater is
_last=${!#} # *indirect reference* to the $# variable
# or
_last=$BASH_ARGV # official built-in (but takes more typing :)
That's it.
$ cat lastarg
#!/bin/bash
# echo the last arg given:
_last=${!#}
echo $_last
_last=$BASH_ARGV
echo $_last
for x; do
echo $x
done
Output is:
$ lastarg 1 2 3 4 "5 6 7"
5 6 7
5 6 7
1
2
3
4
5 6 7
The following will work for you.
# is for array of arguments.
: means at
$# is the length of the array of arguments.
So the result is the last element:
${#:$#}
Example:
function afunction{
echo ${#:$#}
}
afunction -d -o local 50
#Outputs 50
Note that this is bash-only.
Use indexing combined with length of:
echo ${#:${##}}
Note that this is bash-only.
Found this when looking to separate the last argument from all the previous one(s).
Whilst some of the answers do get the last argument, they're not much help if you need all the other args as well. This works much better:
heads=${#:1:$#-1}
tail=${#:$#}
Note that this is bash-only.
This works in all POSIX-compatible shells:
eval last=\${$#}
Source: http://www.faqs.org/faqs/unix-faq/faq/part2/section-12.html
Here is mine solution:
pretty portable (all POSIX sh, bash, ksh, zsh) should work
does not shift original arguments (shifts a copy).
does not use evil eval
does not iterate through the whole list
does not use external tools
Code:
ntharg() {
shift $1
printf '%s\n' "$1"
}
LAST_ARG=`ntharg $# "$#"`
From oldest to newer solutions:
The most portable solution, even older sh (works with spaces and glob characters) (no loop, faster):
eval printf "'%s\n'" "\"\${$#}\""
Since version 2.01 of bash
$ set -- The quick brown fox jumps over the lazy dog
$ printf '%s\n' "${!#} ${#:(-1)} ${#: -1} ${#:~0} ${!#}"
dog dog dog dog dog
For ksh, zsh and bash:
$ printf '%s\n' "${#: -1} ${#:~0}" # the space beetwen `:`
# and `-1` is a must.
dog dog
And for "next to last":
$ printf '%s\n' "${#:~1:1}"
lazy
Using printf to workaround any issues with arguments that start with a dash (like -n).
For all shells and for older sh (works with spaces and glob characters) is:
$ set -- The quick brown fox jumps over the lazy dog "the * last argument"
$ eval printf "'%s\n'" "\"\${$#}\""
The last * argument
Or, if you want to set a last var:
$ eval last=\${$#}; printf '%s\n' "$last"
The last * argument
And for "next to last":
$ eval printf "'%s\n'" "\"\${$(($#-1))}\""
dog
If you are using Bash >= 3.0
echo ${BASH_ARGV[0]}
For bash, this comment suggested the very elegant:
echo "${#:$#}"
To silence shellcheck, use:
echo ${*:$#}
As a bonus, both also work in zsh.
shift `expr $# - 1`
echo "$1"
This shifts the arguments by the number of arguments minus 1, and returns the first (and only) remaining argument, which will be the last one.
I only tested in bash, but it should work in sh and ksh as well.
I found #AgileZebra's answer (plus #starfry's comment) the most useful, but it sets heads to a scalar. An array is probably more useful:
heads=( "${#: 1: $# - 1}" )
tail=${#:${##}}
Note that this is bash-only.
Edit: Removed unnecessary $(( )) according to #f-hauri's comment.
A solution using eval:
last=$(eval "echo \$$#")
echo $last
If you want to do it in a non-destructive way, one way is to pass all the arguments to a function and return the last one:
#!/bin/bash
last() {
if [[ $# -ne 0 ]] ; then
shift $(expr $# - 1)
echo "$1"
#else
#do something when no arguments
fi
}
lastvar=$(last "$#")
echo $lastvar
echo "$#"
pax> ./qq.sh 1 2 3 a b
b
1 2 3 a b
If you don't actually care about keeping the other arguments, you don't need it in a function but I have a hard time thinking of a situation where you would never want to keep the other arguments unless they've already been processed, in which case I'd use the process/shift/process/shift/... method of sequentially processing them.
I'm assuming here that you want to keep them because you haven't followed the sequential method. This method also handles the case where there's no arguments, returning "". You could easily adjust that behavior by inserting the commented-out else clause.
For tcsh:
set X = `echo $* | awk -F " " '{print $NF}'`
somecommand "$X"
I'm quite sure this would be a portable solution, except for the assignment.
After reading the answers above I wrote a Q&D shell script (should work on sh and bash) to run g++ on PGM.cpp to produce executable image PGM. It assumes that the last argument on the command line is the file name (.cpp is optional) and all other arguments are options.
#!/bin/sh
if [ $# -lt 1 ]
then
echo "Usage: `basename $0` [opt] pgm runs g++ to compile pgm[.cpp] into pgm"
exit 2
fi
OPT=
PGM=
# PGM is the last argument, all others are considered options
for F; do OPT="$OPT $PGM"; PGM=$F; done
DIR=`dirname $PGM`
PGM=`basename $PGM .cpp`
# put -o first so it can be overridden by -o specified in OPT
set -x
g++ -o $DIR/$PGM $OPT $DIR/$PGM.cpp
The following will set LAST to last argument without changing current environment:
LAST=$({
shift $(($#-1))
echo $1
})
echo $LAST
If other arguments are no longer needed and can be shifted it can be simplified to:
shift $(($#-1))
echo $1
For portability reasons following:
shift $(($#-1));
can be replaced with:
shift `expr $# - 1`
Replacing also $() with backquotes we get:
LAST=`{
shift \`expr $# - 1\`
echo $1
}`
echo $LAST
echo $argv[$#argv]
Now I just need to add some text because my answer was too short to post. I need to add more text to edit.
This is part of my copy function:
eval echo $(echo '$'"$#")
To use in scripts, do this:
a=$(eval echo $(echo '$'"$#"))
Explanation (most nested first):
$(echo '$'"$#") returns $[nr] where [nr] is the number of parameters. E.g. the string $123 (unexpanded).
echo $123 returns the value of 123rd parameter, when evaluated.
eval just expands $123 to the value of the parameter, e.g. last_arg. This is interpreted as a string and returned.
Works with Bash as of mid 2015.
To return the last argument of the most recently used command use the special parameter:
$_
In this instance it will work if it is used within the script before another command has been invoked.
#! /bin/sh
next=$1
while [ -n "${next}" ] ; do
last=$next
shift
next=$1
done
echo $last
Try the below script to find last argument
# cat arguments.sh
#!/bin/bash
if [ $# -eq 0 ]
then
echo "No Arguments supplied"
else
echo $* > .ags
sed -e 's/ /\n/g' .ags | tac | head -n1 > .ga
echo "Last Argument is: `cat .ga`"
fi
Output:
# ./arguments.sh
No Arguments supplied
# ./arguments.sh testing for the last argument value
Last Argument is: value
Thanks.
There is a much more concise way to do this. Arguments to a bash script can be brought into an array, which makes dealing with the elements much simpler. The script below will always print the last argument passed to a script.
argArray=( "$#" ) # Add all script arguments to argArray
arrayLength=${#argArray[#]} # Get the length of the array
lastArg=$((arrayLength - 1)) # Arrays are zero based, so last arg is -1
echo ${argArray[$lastArg]}
Sample output
$ ./lastarg.sh 1 2 buckle my shoe
shoe
Using parameter expansion (delete matched beginning):
args="$#"
last=${args##* }
It's also easy to get all before last:
prelast=${args% *}
$ echo "${*: -1}"
That will print the last argument
With GNU bash version >= 3.0:
num=$# # get number of arguments
echo "${!num}" # print last argument
Just use !$.
$ mkdir folder
$ cd !$ # will run: cd folder

Resources