Need to printf script's arguments - bash

I'm trying to list script's argument by printf function. Arguments are counted by iteration of $i. What should be in printf function?
I need something like
eval echo \$$i
but in printf function.
Edit: Have while cycle with iteration of $i and among other code, I have
printf "%s" $i
But, instead of $i, I need some code, that shows me value of argument.
In my case, it is name of file, and I need list them. One file in one iteration.

As noted in a comment, you normally do that with a loop such as:
i=1
for arg in "$#"
do
echo "$i: [$arg]"
((i++))
done
(If echo isn't allowed, use printf "%s\n" … where the … is whatever would have followed echo.)
You might also use indirect expansion to avoid the use of eval:
for i in 1 2 3 4; do echo "$i: [${!i}]"; done
You can generalize that with:
for i in $(seq 1 $#); do echo "$i: [${!i}]"; done
or
for ((i = 1; i <= $#; i++)); do echo "$i: [${!i}]"; done
For example, given:
set -- a b 'c d' ' e f '
all the loops produce the output:
1: [a]
2: [b]
3: [c d]
4: [ e f ]
The square brackets are merely there to delimit the argument values; it allows you to see the trailing blanks on the fourth line of output.
You might also be able to use:
printf "[%s]\n" "$#"
to get:
[a]
[b]
[c d]
[ e f ]

It is not very clear what you are asking for, but this will list the arguments passed to the script:
while [ $# -gt 0 ]; do
printf "%s\n" "$1"
shift
done

I guess what you are asking for is how to make the indirect reference to positional parameter i (i containing the position):
print "%s\n" ${!i}

To get your arguments in such a way that they can be fed back into the shell with the exact same value, the following bash extension can be used:
printf '%q ' "$#"; printf '\n'
This works even for rather unusual cases. Let's say that one of your arguments contains a newline:
./your-script 'hello
world' 'goodbye world'
This will be represented in the printf output:
$'hello\nworld' goodbye\ world
...with something you can use again in the shell:
$ echo $'hello\nworld' goodbye\ world
hello
world goodbye world

Related

bash read strings and output as one key and multiple values

Assuming there is an input:
1,2,C
We are trying to output it as
KEY=1, VAL1=2, VAL2=C
So far trying to modify from here:
Is there a way to create key-value pairs in Bash script?
for i in 1,2,C ; do KEY=${i%,*,*}; VAL1=${i#*,}; VAL2=${i#*,*,}; echo $KEY" XX "$VAL1 XX "$VAL2"; done
Output:
1 XX 2,c XX c
Not entirely sure what the pound ("#") and % here mean above, making the modification kinda hard.
Could any guru enlighten? Thanks.
I would generally prefer easier to read code, as bash can get ugly pretty fast.
Try this:
key_values.sh
#!/bin/bash
IFS=,
count=0
# $* is the expansion of all the params passed in, i.e. $1, $2, $3, ...
for i in $*; do
# '-eq' is checking for equality, i.e. is $count equal to zero.
if [ $count -eq 0 ]; then
echo -n "KEY=$i"
else
echo -n ", VAL${count}=$i"
fi
count=$(( $count + 1 ))
done
echo
Example
key_values.sh 1,2,ABC,123,DEF
Output
KEY=1, VAL1=2, VAL2=ABC, VAL3=123, VAL4=DEF
Expanding on anishsane's comment:
$ echo $1
1,2,3,4,5
$ IFS=, read -ra args <<<"$1" # read into an array
$ out="KEY=${args[0]}"
$ for ((i=1; i < ${#args[#]}; i++)); do out+=", VAL$i=${args[i]}"; done
$ echo "$out"
KEY=1, VAL1=2, VAL2=3, VAL3=4, VAL4=5

in bash,now i have two strings,one is 'a b c' while another is '1 2 3' any good way to combine them to 'a=1 b=2 c=3' I

in bash,now i have two strings,one is 'a b c' while another is '1 2 3'
any good way to combine them to 'a=1 b=2 c=3'
I tried string to array and combined them.but if i don't know the IFS?
IFS=' ' read -r -a array1 <<< "$upvote_count"
IFS=' ' read -r -a array0 <<< "$qids"
tLen=${#array[#]}
for (( i=0; i<${tLen}; i++ ));
do
echo "${array0[$i]}"" ""${array1[$i]}">>a.txt
done
You can create arrays from each string[1] and then use eval to create the variables with names from string 1 and values from string 2 by looping over each array element and evaluating array1[i]=array2[i] (pseudocode). A short script would look like the following:
#!/bin/bash
v1="a b c" ## original string variables
v2="1 2 3"
ar1=( $(echo $v1) ) ## create arrays 1 & 2 from strings
ar2=( $(echo $v2) )
for ((i=0; i<${#ar1[#]}; i++)); do
eval "${ar1[i]}=${ar2[i]}" ## eval to create variables
done ## (be careful with eval)
printf "a=%s\nb=%s\nc=%s\n" $a $b $c ## confirm
Output
$ bash evalabc.sh
a=1
b=2
c=3
You would want to add validations that the you have the same number of elements in each array, that the elements of the first don't begin with numbers, etc.. and that they do not contain anything that would be harmful when you run eval!
As noted in the comment of the script, take great care in using eval (or avoid it altogether) because it will do exactly what you tell it to do. You would not want to have, e.g. a=sudo rm, b="-rf", c=/* and then eval "$a $b $c" -- very bad things can happen.
Footnotes:
[1] (adjust IFS as needed - not needed for space separation)
Give this tested version a try:
unset letters; unset figures; IFS=' ' read -r -a letters <<< "a b c" ; \
IFS=' ' read -r -a figures <<< '1 2 3' ; \
for i in "${!letters[#]}" ; do \
printf "%s=%s\n" ${letters[i]} ${figures[i]}; \
done
a=1
b=2
c=3
A general method that works in any Bourne shell, (not just bash): Use a for loop for one list (a b c), match it up with piped input for the second list (1 2 3), produce a string of shell code, and eval that.
eval $(seq 3 | for f in a b c ; do read x ; echo $f=$x ; done) ;
echo $a $b $c
which prints:
1 2 3
eval is needed because variables assigned in a loop are forgotten once the loop is over.
Caution: never let eval execute unknown code. If either list contained unwanted shell code delimiters or commands, further parsing would be necessary, i.e. a prophylactic pipe after '; done'.
Applied to the specific example in the starting question, with two strings, containing space separated lists, we get:
qids="a b c" # our variable names
unset $qids # clear them if need be
upvote_count="1 2 3" # the values to be assigned
# generate code to assign the names list to the values list,
# via a 'for' loop, the index $var_name is for the $qids,
# and assign $var_name to each $upvote_count value which we pipe in.
# Since an assignment can't leave the loop, we 'echo'
# the code for assignment, and 'eval' that code after the loop
# is done.
eval $(
echo "${upvote_count}" |
tr ' ' '\n' |
for var_name in $qids ; do
read value
echo "$var_name=$value"
done
)
# The loop is done, so test if the code worked:
for var_name in $qids
do
echo -n $var_name=
eval echo \$$var_name
done
...which outputs:
a=1
b=2
c=3

How to fetch last argument and stop before last arguments in shell script?

I want to merge all files into one. Here, the last argument is the destination file name.
I want to take last argument and then in loop stop before last arguments.
Here code is given that I want to implement:
echo "No. of Argument : $#"
for i in $* - 1
do
echo $i
cat $i >> last argument(file)
done
How to achieve that?
Using bash:
fname=${!#}
for a in "${#:1:$# - 1}"
do
echo "$a"
cat "$a" >>"$fname"
done
In bash, the last argument to a script is ${!#}. So, that is where we get the file name.
bash also allows selecting elements from an array. To start with a simple example, observe:
$ set -- a b c d e f
$ echo "${#}"
a b c d e f
$ echo "${#:2:4}"
b c d e
In our case, we want to select elements from the first to the second to last. The first is number 1. The last is number $#. We want to select all but the last. WE thus want $# - 1 elements of the array. Therefore, to select the arguments from the first to the second to last, we use:
${#:1:$# - 1}
A POSIX-compliant method:
eval last_arg=\$$#
while [ $# -ne 1 ]; do
echo "$1"
cat "$1" >> "$last_arg"
shift
done
Here, eval is safe, because you are only expanding a read-only parameter in the string that eval will execute. If you don't want to unset the positional parameters via shift, you can iterate over them, using a counter to break out of the loop early.
eval last_arg=\$$#
i=1
for arg in "$#"; do
echo "$arg"
cat "$arg" >> "$last_arg"
i=$((i+1))
if [ "$i" = "$#" ]; then
break
fi
done

Accessing the last argument passed in a bash function, then shifting it

I have a simple function in bash which I want to take any number of arguments. What I'd like to do is echo the last argument passed to the function, and then shift, and repeat until I am out of arguments.
Here is my code:
test(){
while [ $# -ne 0 ]
do
echo "${#: -1}"
shift
done
}
However, the shift doesn't seem to take place.
If I run:
test a b c d
it will return
d
d
d
d
Whereas I obviously would like to return
d
c
b
a
What am I doing wrong? Thanks! :)
shift doesn't remove the last argument, it removes the first one. $2 becomes $1, $3 becomes $2, etc. d will always be the last argument because items are being popped off the front.
If you want to print all the arguments in reverse order, I'd suggest:
for ((i = $#; i > 0; --i)); do
echo "${!i}"
done
Alternatively, you could mimic shifting from the right side with:
while (($# > 0)); do
echo "${#: -1}"
set -- "${#:1:$#-1}"
done
Also, another way to write ${#: -1} is ${!#}. That might be too clever, though. That kind of trickery is something you'd see in some inscrutable Perl script.
Another way:
revargs() {
local sgra=()
for arg; do sgra=("$arg" "${sgra[#]}"); done
printf "%s\n" "${sgra[#]}"
}
revargs foo bar baz qux
qux
baz
bar
foo
This just occurred to me: it's pretty cheesy, but it's short.
revargs() {
printf "%s\n" "$#" | tac
}

parse and expand interval

In my script I need to expand an interval, e.g.:
input: 1,5-7
to get something like the following:
output: 1,5,6,7
I've found other solutions here, but they involve python and I can't use it in my script.
Solution with Just Bash 4 Builtins
You can use Bash range expansions. For example, assuming you've already parsed your input you can perform a series of successive operations to transform your range into a comma-separated series. For example:
value1=1
value2='5-7'
value2=${value2/-/..}
value2=`eval echo {$value2}`
echo "input: $value1,${value2// /,}"
All the usual caveats about the dangers of eval apply, and you'd definitely be better off solving this problem in Perl, Ruby, Python, or AWK. If you can't or won't, then you should at least consider including some pipeline tools like tr or sed in your conversions to avoid the need for eval.
Try something like this:
#!/bin/bash
for f in ${1//,/ }; do
if [[ $f =~ - ]]; then
a+=( $(seq ${f%-*} 1 ${f#*-}) )
else
a+=( $f )
fi
done
a=${a[*]}
a=${a// /,}
echo $a
Edit: As #Maxim_united mentioned in the comments, appending might be preferable to re-creating the array over and over again.
This should work with multiple ranges too.
#! /bin/bash
input="1,5-7,13-18,22"
result_str=""
for num in $(tr ',' ' ' <<< "$input"); do
if [[ "$num" == *-* ]]; then
res=$(seq -s ',' $(sed -n 's#\([0-9]\+\)-\([0-9]\+\).*#\1 \2#p' <<< "$num"))
else
res="$num"
fi
result_str="$result_str,$res"
done
echo ${result_str:1}
Will produce the following output:
1,5,6,7,13,14,15,16,17,18,22
expand_commas()
{
local arg
local st en i
set -- ${1//,/ }
for arg
do
case $arg in
[0-9]*-[0-9]*)
st=${arg%-*}
en=${arg#*-}
for ((i = st; i <= en; i++))
do
echo $i
done
;;
*)
echo $arg
;;
esac
done
}
Usage:
result=$(expand_commas arg)
eg:
result=$(expand_commas 1,5-7,9-12,3)
echo $result
You'll have to turn the separated words back into commas, of course.
It's a bit fragile with bad inputs but it's entirely in bash.
Here's my stab at it:
input=1,5-7,10,17-20
IFS=, read -a chunks <<< "$input"
output=()
for chunk in "${chunks[#]}"
do
IFS=- read -a args <<< "$chunk"
if (( ${#args[#]} == 1 )) # single number
then
output+=(${args[*]})
else # range
output+=($(seq "${args[#]}"))
fi
done
joined=$(sed -e 's/ /,/g' <<< "${output[*]}")
echo $joined
Basically split on commas, then interpret each piece. Then join back together with commas at the end.
A generic bash solution using the sequence expression `{x..y}'
#!/bin/bash
function doIt() {
local inp="${#/,/ }"
declare -a args=( $(echo ${inp/-/..}) )
local item
local sep
for item in "${args[#]}"
do
case ${item} in
*..*) eval "for i in {${item}} ; do echo -n \${sep}\${i}; sep=, ; done";;
*) echo -n ${sep}${item};;
esac
sep=,
done
}
doIt "1,5-7"
Should work with any input following the sample in the question. Also with multiple occurrences of x-y
Use only bash builtins
Using ideas from both #Ansgar Wiechers and #CodeGnome:
input="1,5-7,13-18,22"
for s in ${input//,/ }
do
if [[ $f =~ - ]]
then
a+=( $(eval echo {${s//-/..}}) )
else
a+=( $s )
fi
done
oldIFS=$IFS; IFS=$','; echo "${a[*]}"; IFS=$oldIFS
Works in Bash 3
Considering all the other answers, I came up with this solution, which does not use any sub-shells (but one call to eval for brace expansion) or separate processes:
# range list is assumed to be in $1 (e.g. 1-3,5,9-13)
# convert $1 to an array of ranges ("1-3" "5" "9-13")
IFS=,
local range=($1)
unset IFS
list=() # initialize result list
local r
for r in "${range[#]}"; do
if [[ $r == *-* ]]; then
# if the range is of the form "x-y",
# * convert to a brace expression "{x..y}",
# * using eval, this gets expanded to "x" "x+1" … "y" and
# * append this to the list array
eval list+=( {${r/-/..}} )
else
# otherwise, it is a simple number and can be appended to the array
list+=($r)
fi
done
# test output
echo ${list[#]}

Resources