Using a positional parameter as the name of the array I want to loop over - bash

I have been looking at other similar questions but didn't find the proper answer. $1 is the name of the array i want to loop over.
#!/bin/bash
for i in ${"$1"[#]}
do
echo "$i"
done

If I don't misunderstand your not very well written question, you are looking for bash indirection, which is available since bash, version 2.
Below a minimal example
#! /bin/bash
foo=( 1 2 3 4 )
bar=( 5 6 7 8 )
if [[ ( "$1" -ne foo && "$1" -ne bar ) ]];
then
echo "Usage: $0 <foo|bar>"
exit 1
fi
ambito="$1"[#] # here what you are looking for
for i in "${!ambito}"; # here the indirection
do
echo -n "$i "
done
echo ""
And you can call this indirection.sh scrit as:
$ ./indirection.sh foo
1 2 3 4
or
$ ./indirection.sh bar
5 6 7 8
This being said, using indirection may cause confusion. In most cases it could be replaced by associative arrays.

$1 is the first command line argument, not all of them. Probably what you are looking for is $#.
And to create an array holding them, something like:
ambito=($#)

Related

In bash how to use the last argument- and adding all other arguments to array

I have a script where the user can add as many arguments as he would like (numbers).
The script will sum all the numbers beside the last number - The last number (argument) is the number that I need to divide by
For example:
./test.sh 2 2 6 5
This will sum the first 3 numbers (2+2+6) and divide the answer by 5 (the last argument)
How can I use the last argument? Echo ????
How can I move loop the first arguments besides the last one – I would like that all 3 arguments will be added to an array and I can loop it
Please note that the number of arguments can be changed
How can I use the last argument? Echo ????
Granting $# > 0, you can use "${!#}".
How can I move loop the first arguments besides the last one – I would
like that all 3 arguments will be added to an array and I can loop it
Again granting $# > 0, you can refer to "${#:1:$# - 1}".
Read the Arrays section in the bash manual to know how to properly expand arrays.
I also recommend learning how quoting works and knowing the dangers of unwanted word splitting and globbing.
Shortly (with bashisms)
As this question is tagged integer-arithmetic and bash:
Here is a small and efficient script:
#!/bin/bash
addVals=${*: 1 : $# - 1}
declare -i intResult=" ( ${addVals// /+} ) / ${#: -1} "
echo $intResult
But there's no loop...
Long answer
How can I use the last argument? Echo ????
You could make your tries in command line:
set -- 2 2 6 5
Then
echo $#
2 2 6 5
echo ${*: 3}
6 5
echo ${*: -1}
5
echo ${*: 1 : -1}
bash: -1: substring expression < 0
echo $#
4
echo ${*: 1 : $# -1}
2 2 6
Ok, then
someVar=${*: 1 : $# -1}
echo ${someVar// /:SpaceReplacment:}
2:SpaceReplacment:2:SpaceReplacment:6
so
declare -i result
result=" ( ${someVar// /+} ) / ${*: -1} "
echo $result
2
How can I move loop the first arguments besides the last one – I would like that all 3 arguments will be added to an array and I can loop it
Still forward, under command line...
someArray=("${#: 1: $# -1 }")
Use declare -p to show $someArray's content:
declare -p someArray
declare -a someArray=([0]="2" [1]="2" [2]="6")
Then
declare -i mySum=0
for i in "${someArray[#]}";do
mySum+=i
done
echo $mySum
10
echo $(( mySum / ${*: -1} ))
2
Please note that the number of arguments can be changed
Please note:
Using double quotes allow processing of strings containing spaces:
set -- foo bar 'foo bar baz'
echo ${2}
bar
echo ${*: $# }
foo bar baz
Difference betweeen use of "$#" (array to array) and "$*" (array to string)
set -- foo bar 'foo bar' 'foo bar baz'
If I take 3 first elements:
someArray=("${#: 1: $# -1 }")
declare -p someArray
declare -a someArray=([0]="foo" [1]="bar" [2]="foo bar")
But
someArray=("${*: 1: $# -1 }")
declare -p someArray
declare -a someArray=([0]="foo bar foo bar")
There are about a thousand ways of doing this. As you would like to make use of integer arithmetic, you can do the following in bash
A short semi-cryptic version would be:
IFS=+
echo $(( ( ${*} - ${#:-1} ) / ${#:-1} ))
Here we make use of the difference between "${*}" and "${#}" to perform the sum by setting IFS=+ (See What is the difference between "$#" and "$*" in Bash?)
A long classic approach would be:
for i in "$#"; do ((s+=i)); done
echo $(( (s-${#:-1})/${#:-1} ))
It's easier to sum all terms and subtract the last term afterwards

Check if every argument is an integer in shell

I've been killing myself over this trying to figure it out, and I know it's probably super simple, so hoping a new pair of eyes can help. I have a Bourne shell (sh) script that I'm writing and it takes a list of integers as the input (and amount from 1 integer up, ideally I'd like to take both positive and negative ints). I'm trying to do an error check for the case if someone inputs something other than an integer. They could input "1 2 3 4 5 a" and it'd give an error, because the a is not an int.
I have an error check for no inputs that works, and I have code that does stuff to the list of integers themselves, but even when strings are given it still gets to my final code block.
I currently have a for loop to iterate through each item in the list of integers, then an if loop to give the error message if the argument in question isn't an int. I've tried a few different versions of this, but this is the most recent one so I've put it below.
for i in $#; do
if [ $i -ge 0 ] 2>/dev/null; then
echo "Invalid input: integers only."
exit 1
fi
done
#!/bin/sh
#
for i in "$#"
do
case "${i#[-+]}" in
0)
echo cannot be 0?
exit 1
;;
*[!0-9]* | '')
echo not int
exit 2
;;
esac
done
echo i\'m ok
This should work, for both positive and negative ints. And if you admit that 0 is an integer, just delete the first case.
Almost duplicate: BASH: Test whether string is valid as an integer?
And here is a good answer for posix. https://stackoverflow.com/a/18620446/7714132
You could use a regex:
my_script.sh
for i in $# ; do
if ! [[ "$i" =~ ^-?[0-9]+$ ]] ; then
echo "Invalid input: integers only."
exit 1
fi
done
Example:
$ sh my_script.sh 1 2 3 4
$ sh my_script.sh 1 2 -12
$ sh my_script.sh 1 2 -12-2
Invalid input: integers only.
$ sh my_script.sh 1 2 a b
Invalid input: integers only.
Explanation of the regex:
^: beginning of the string
-?: 0 or 1 times the character -
[0-9]+: 1 or more digit
$: end of the string
In POSIX sh, you can match your string against a glob with case:
#!/bin/sh
for i
do
case "$i" in
*[!0-9]*)
echo "Integers only" >&2
exit 1
esac
done
echo "Ok"
Here's how it runs:
$ ./myscript 1 2 3 4 5
Ok
$ ./myscript 1 2 3 4 5 a
Integers only
The problem with your approach is primarily that you're checking for success rather than failure: [ will fail when the input is invalid.

Is it possible to use a variable in for syntax in bash?

I wonder If it is possible to write "for i in {n..k}" loop with a variable.
For example;
for i in {1..5}; do
echo $i
done
This outputs
1
2
3
4
5
On the other hands
var=5
for i in {1..$var}; do
echo $i
done
prints
{1..5}
How can I make second code run as same as first one?
p.s. I know there is lots of way to create a loop by using a variable but I wanted to ask specifically about this syntax.
It is not possible to use variables in the {N..M} syntax. Instead, what you can do is use seq:
$ var=5
$ for i in $(seq 1 $var) ; do echo "$i"; done
1
2
3
4
5
Or...
$ start=3
$ end=8
$ for i in $(seq $start $end) ; do echo $i; done
3
4
5
6
7
8
While seq is fine, it can cause problems if the value of $var is very large, as the entire list of values needs to be generated, which can cause problems if the resulting command line is too long. bash also has a C-style for loop which doesn't explicitly generate the list:
for ((i=1; i<=$var; i++)); do
echo "$i"
done
(This applies to constant sequences as well, since {1..10000000} would also generate a very large list which could overflow the command line.)
You can use eval for this:
$ num=5
$ for i in $(eval echo {1..$num}); do echo $i; done
1
2
3
4
5
Please read drawbacks of eval before using.

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

Extract parameters before last parameter in "$#"

I'm trying to create a Bash script that will extract the last parameter given from the command line into a variable to be used elsewhere. Here's the script I'm working on:
#!/bin/bash
# compact - archive and compact file/folder(s)
eval LAST=\$$#
FILES="$#"
NAME=$LAST
# Usage - display usage if no parameters are given
if [[ -z $NAME ]]; then
echo "compact <file> <folder>... <compressed-name>.tar.gz"
exit
fi
# Check if an archive name has been given
if [[ -f $NAME ]]; then
echo "File exists or you forgot to enter a filename. Exiting."
exit
fi
tar -czvpf "$NAME".tar.gz $FILES
Since the first parameters could be of any number, I have to find a way to extract the last parameter, (e.g. compact file.a file.b file.d files-a-b-d.tar.gz). As it is now the archive name will be included in the files to compact. Is there a way to do this?
To remove the last item from the array you could use something like this:
#!/bin/bash
length=$(($#-1))
array=${#:1:$length}
echo $array
Even shorter way:
array=${#:1:$#-1}
But arays are a Bashism, try avoid using them :(.
Portable and compact solutions
This is how I do in my scripts
last=${#:$#} # last parameter
other=${*%${!#}} # all parameters except the last
EDIT
According to some comments (see below), this solution is more portable than others.
Please read Michael Dimmitt's commentary for an explanation of how it works.
last_arg="${!#}"
Several solutions have already been posted; however I would advise restructuring your script so that the archive name is the first parameter rather than the last. Then it's really simple, since you can use the shift builtin to remove the first parameter:
ARCHIVENAME="$1"
shift
# Now "$#" contains all of the arguments except for the first
Thanks guys, got it done, heres the final bash script:
#!/bin/bash
# compact - archive and compress file/folder(s)
# Extract archive filename for variable
ARCHIVENAME="${!#}"
# Remove archive filename for file/folder list to backup
length=$(($#-1))
FILES=${#:1:$length}
# Usage - display usage if no parameters are given
if [[ -z $# ]]; then
echo "compact <file> <folder>... <compressed-name>.tar.gz"
exit
fi
# Tar the files, name archive after last file/folder if no name given
if [[ ! -f $ARCHIVENAME ]]; then
tar -czvpf "$ARCHIVENAME".tar.gz $FILES; else
tar -czvpf "$ARCHIVENAME".tar.gz "$#"
fi
Just dropping the length variable used in Krzysztof Klimonda's solution:
(
set -- 1 2 3 4 5
echo "${#:1:($#-1)}" # 1 2 3 4
echo "${#:(-$#):($#-1)}" # 1 2 3 4
)
I would add this as a comment, but don't have enough reputation and the answer got a bit longer anyway. Hope it doesn't mind.
As #func stated:
last_arg="${!#}"
How it works:
${!PARAM} indicates level of indirection. You are not referencing PARAM itself, but the value stored in PARAM ( think of PARAM as pointer to value ).
${#} expands to the number of parameters (Note: $0 - the script name - is not counted here).
Consider following execution:
$./myscript.sh p1 p2 p3
And in the myscript.sh
#!/bin/bash
echo "Number of params: ${#}" # 3
echo "Last parameter using '\${!#}': ${!#}" # p3
echo "Last parameter by evaluating positional value: $(eval LASTP='$'${#} ; echo $LASTP)" # p3
Hence you can think of ${!#} as a shortcut for the above eval usage, which does exactly the approach described above - evaluates the value stored in the given parameter, here the parameter is 3 and holds the positional argument $3
Now if you want all the params except the last one, you can use substring removal ${PARAM%PATTERN} where % sign means 'remove the shortest matching pattern from the end of the string'.
Hence in our script:
echo "Every parameter except the last one: ${*%${!#}}"
You can read something in here: Parameter expansion
Are you sure this fancy script is any better than a simple alias to tar?
alias compact="tar -czvpf"
Usage is:
compact ARCHIVENAME FILES...
Where FILES can be file1 file2 or globs like *.html
Try:
if [ "$#" -gt '0' ]; then
/bin/echo "${!#}" "${#:1:$(($# - 1))}
fi
Array without last parameter:
array=${#:1:$#-1}
But it's a bashism :(. Proper solutions would involve shift and adding into variable as others use.
#!/bin/bash
lastidx=$#
lastidx=`expr $lastidx - 1`
eval last='$'{$lastidx}
echo $last
Alternative way to pull the last parameter out of the argument list:
eval last="\$$#"
eval set -- `awk 'BEGIN{for(i=1;i<'$#';i++) printf " \"$%d\"",i;}'`
#!/bin/sh
eval last='$'$#
while test $# -gt 1; do
list="$list $1"
shift
done
echo $list $last
I can't find a way to use array-subscript notation on $#, so this is the best I can do:
#!/bin/bash
args=("$#")
echo "${args[$(($#-1))]}"
This script may work for you - it returns a subrange of the arguments, and can be called from another script.
Examples of it running:
$ args_get_range 2 -2 y a b "c 1" d e f g
'b' 'c 1' 'd' 'e'
$ args_get_range 1 2 n arg1 arg2
arg1 arg2
$ args_get_range 2 -2 y arg1 arg2 arg3 "arg 4" arg5
'arg2' 'arg3'
$ args_get_range 2 -1 y arg1 arg2 arg3 "arg 4" arg5
'arg2' 'arg3' 'arg 4'
# You could use this in another script of course
# by calling it like so, which puts all
# args except the last one into a new variable
# called NEW_ARGS
NEW_ARGS=$(args_get_range 1 -1 y "$#")
args_get_range.sh
#!/usr/bin/env bash
function show_help()
{
IT="
Extracts a range of arguments from passed in args
and returns them quoted or not quoted.
usage: START END QUOTED ARG1 {ARG2} ...
e.g.
# extract args 2-3
$ args_get_range.sh 2 3 n arg1 arg2 arg3
arg2 arg3
# extract all args from 2 to one before the last argument
$ args_get_range.sh 2 -1 n arg1 arg2 arg3 arg4 arg5
arg2 arg3 arg4
# extract all args from 2 to 3, quoting them in the response
$ args_get_range.sh 2 3 y arg1 arg2 arg3 arg4 arg5
'arg2' 'arg3'
# You could use this in another script of course
# by calling it like so, which puts all
# args except the last one into a new variable
# called NEW_ARGS
NEW_ARGS=\$(args_get_range.sh 1 -1 \"\$#\")
"
echo "$IT"
exit
}
if [ "$1" == "help" ]
then
show_help
fi
if [ $# -lt 3 ]
then
show_help
fi
START=$1
END=$2
QUOTED=$3
shift;
shift;
shift;
if [ $# -eq 0 ]
then
echo "Please supply a folder name"
exit;
fi
# If end is a negative, it means relative
# to the last argument.
if [ $END -lt 0 ]
then
END=$(($#+$END))
fi
ARGS=""
COUNT=$(($START-1))
for i in "${#:$START}"
do
COUNT=$((COUNT+1))
if [ "$QUOTED" == "y" ]
then
ARGS="$ARGS '$i'"
else
ARGS="$ARGS $i"
fi
if [ $COUNT -eq $END ]
then
echo $ARGS
exit;
fi
done
echo $ARGS
This works for me, with sh and bash:
last=${*##* }
others=${*%${*##* }}

Resources