Bash arrays and negative subscripts, yes or no? - linux

The GNU bash manual tells me
An indexed array is created automatically if any variable is assigned
to using the syntax
name[subscript]=value
The subscript is treated as an arithmetic expression that must
evaluate to a number. If subscript evaluates to a number less than
zero, it is used as an offset from one greater than the array’s
maximum index (so a subcript of -1 refers to the last element of the
array).
So I figure I will give it a try and get the following result:
$ muh=(1 4 'a' 'bleh' 2)
$ echo $muh
1
$ echo ${muh[*]}
1 4 a bleh 2 # so far so good so now I'll try a negative ...
$ echo ${muh[-1]}
-bash: muh: bad array subscript # didn't go as planned!
Did I do something wrong, or is the website wrong, or is gnu bash that different from the bash I am running under CentOS? Thanks!

If you just want the last element
$ echo ${muh[*]: -1}
2
If you want next to last element
$ echo ${muh[*]: -2:1}
bleh

According to Greg Wooledge's wiki, (which links to the bash changelog) the negative index syntax was added to bash in version 4.2 alpha.

Bash beore 4.2 (like the default one on Macs these days) doesn't support negative subscripts. Apart from the "substring expansion" used in the accepted answer, a possibly cleaner workaround is to count the desired index from the array start within the brackets:
$ array=(one two three)
$ echo "${array[${#array[#]}-1]}"
three
With this approach, you can pack other parameter expansion operations into the term, e.g. "remove matching prefix pattern" th:
$ echo "${array[${#array[#]}-1]#th}"
ree

If you do man bash the section on arrays does not list this behavior. It might be something new (gnu?) in bash.
Fails for me in CentOS 6.3 (bash 4.1.2)

The negative subscript works perfectly fine for me on my computer with Ubuntu 14.04 / GNU bash version 4.3.11(1) however it returns:
line 46: [-1]: bad array subscript
When I try to run the same script on 4.2.46(1). I

Related

How do I specify the position of a width parameter?

Apologies if this has been posted before. I've been searching all over and can't find an answer.
According to man printf, "FORMAT controls the output as in C printf." It refers to printf(3).
According to man 3 printf, you can specify a variable width in a given position. It says:
Instead of a decimal digit string one may write … "*m$" (for some decimal integer m) to specify that the precision is given in the m-th argument … which must be of type int.
This is the part where I'm struggling. To give a simple example, suppose I wish to print a string with width 14.
$ printf '[%14s]\n' Something
[ Something]
I can use a variable instead:
$ WIDTH=14
$ printf '[%*s]\n' ${WIDTH} Something
[ Something]
The tricky part comes when I want to tell printf that the width argument is in a different position. To keep things simple for this example, I'll leave it where it is, in position 1. Following the instructions, I write the following.
Built-in version:
$ printf '[%*1$s]\n' ${WIDTH} Something
bash: printf: `1': invalid format character
Version in /usr/bin:
$ /usr/bin/printf '[%*1$s]\n' ${WIDTH} Something
[/usr/bin/printf: %*1: invalid conversion specification
Even using the example from the manual gives an error.
Built-in version:
$ printf '%2$*1$d' 6 34
bash: printf: `$': invalid format character
Version in /usr/bin:
$ /usr/bin/printf '%2$*1$d' 6 34
/usr/bin/printf: %2$: invalid conversion specification
As you can see, I get an error every time. I have struggled to see what I'm doing wrong, and I simply cannot find any example online.
How should this be formatted, please, or is the manual just wrong?
Lubuntu 18.04
GNU bash, version 4.4.20(1)-release (x86_64-pc-linux-gnu)
builtin printf: unknown version
/usr/bin/printf printf (GNU coreutils) 8.28
Thank you
No, the extension that comes from Single UNIX Specification to C printf function to specify the position of positional parameter with $ is not supported in bash printf and I doubt it's supported in any of shells printf implementations. I believe the reason is that it requires additional work from the developers, while it's very rarely used extension.
is the manual just wrong?
FORMAT controls the output as in C printf.
You are reading the documentation for GNU version of printf, taken from the amazing Linux man-pages project. I would say C printf is documented in the C standard.
If your shell's printf doesn't support parameter indexing, you can still call Perl to the rescue!
$ perl -we 'printf q([%1$*2$d]), #ARGV' 36 6
[ 36]
See printf and, more importantly, sprintf.

Why does space in Bash string subscripts matter here?

I'm experimenting with bash scripting and noticed the following behavior:
file="test.jar"
echo "${file: -4}" #prints .jar
echo "${file:-4}" #prints test.jar
Very confusing behavior actually. Can someone explain why the second case prints the whole test.jar?
This is due to inconsistent syntax. {"string":-} means default (all the string), whatever follows -. So you need either a space or parenthesis:
{"string": -4}
{"string":(-4)}
Read bash string manipulation.
This is a compromise due to the timeline of features being added to Bash.
The ${parameter:-word} or ${parameter-word} syntax for "replace parameter with word if parameter is null or unset (:-) / null (-)" was around for pretty much always; the - version was already in the Version 7 Bourne Shell.
The ${parameter:offset:length} and ${parameter:offset} syntax for "substring of parameter starting at offset (with optional length length)" was introduced in Bash 2.0 (no conflict so far).
Negative offsets and length specifications for the substring construct were introduced in Bash 4.2. This leads to a problem:
$ string=01234567890abcdefgh
$ echo ${string:7:-2} # Not ambiguous
7890abcdef
$ echo ${string:-7} # Interpreted as "undefined/null, or..."
01234567890abcdefgh
$ echo ${string: -7} # Interpreted as offset from the end
bcdefgh
$ echo ${string:(-7)} # Interpreted as offset from the end
bcdefgh
The space before - or the parentheses around the negative offset are there to tell the expansion apart from the :- (default value) expansion.
If the space is not there, the expansion ${file:-4} is interpreted as "print 4 if the parameter file is null or unset, and the expansion of file otherwise".
References:
BashFAQ/061: Is there a list of which features were added to specific releases (versions) of Bash?
Bash hackers wiki: Bash changes
Shell parameter expansion in the bash manual
Bash NEWS file describing feature added per version

Read filename with * shell bash

I'am new in Linux and I want to write a bash script that can read in a file name of a directory that starts with LED + some numbers.(Ex.: LED5.5.002)
In that directory there is only one file that will starts with LED. The problem is that this file will every time be updated, so the next time it will be for example LED6.5.012 and counting.
I searched and tried a little bit and came to this solution:
export fspec=/home/led/LED*
LedV=`basename $fspec`
echo $LedV
If I give in those commands one by one in my terminal it works fine, LedV= LED5.5.002 but if i run it in a bash scripts it gives the result: LedV = LED*
I search after another solution:
a=/home/led/LED*
LedV=$(basename $a)
echo $LedV
but here again the same, if i give it in one by one it's ok but in a script: LedV = LED*.
It's probably something small but because of my lack of knowledge over Linux I cannot find it. So can someone tell what is wrong?
Thanks! Jan
Shell expansions don't happen on scalar assignments, so in
varname=foo*
the expansion of "$varname" will literally be "foo*". It's more confusing when you consider that echo $varname (or in your case basename $varname; either way without the double quotes) will cause the expansion itself to be treated as a glob, so you may well think the variable contains all those filenames.
Array expansions are another story. You might just want
fspec=( /path/LED* )
echo "${fspec[0]##*/}" # A parameter expansion to strip off the dirname
That will work fine for bash. Since POSIX sh doesn't have arrays like this, I like to give an alternative approach:
for fspec in /path/LED*; do
break
done
echo "${fspec##*/}"
pwd
/usr/local/src
ls -1 /usr/local/src/mysql*
/usr/local/src/mysql-cluster-gpl-7.3.4-linux-glibc2.5-x86_64.tar.gz
/usr/local/src/mysql-dump_test_all_dbs.sql
if you only have 1 file, you will only get 1 result
MyFile=`ls -1 /home/led/LED*`

Dividing in Linux with date in two files

I must take 100 located in File2 and divide it by 5 located in File1. This has to be done in a script. The book is not too clear on how to proceed. I have tried many different iterations of the script but always come back with an error referencing "/". I have put the expression in backtick, double parenthesis and brackets. The OS is Red Hat Linux.
The script:
cat File1 File2
#!/bin/bash
var3=$[$var2 / $var1]
This is what I get:
var1=5
var2=100
/home/Student/MyFiles/student/week3prog3student.scr: line 3: / : syntax error: operand expected (error token is "/ ")
#!/bin/bash
var3=$(($var2 / $var1))
Here's a nifty way of doing floating point division that is scriptable. Bash only does integer division via $((x/y))
var2=10
var1=3
echo "scale=2; $var2/$var1" | bc
scale is the number of decimal digits after the decimal point.

How to encode url in bash script?

EDIT (Side Question)
Can someone please explain what this line does?
eval website=\${$#}
The script reads a lot of paremeters, it's called somewhat like this
./script.sh -t 30 -n 100 -a test http://www.google.com
I have trouble reading the url ( http://www.google.com )
I am opening firefox using urls passed to a bash script. How do I encode them? Some of these urls are causing issue.
Some code
eval website=\${$#} // takes as argument
firefox -width 1280 -height 8000 ${website} &
Problematic URL
http://www.airportbusiness.com//print/Airport-Business-Magazine/Expo-Returns-to-Vegas/1$41912
In firefox, it opens as
http://www.airportbusiness.com//print/Airport-Business-Magazine/Expo-Returns-to-Vegas/141912
$ sign gets removed
The easiest way is probably to escape the characters that cause some problems.
Unless your url contain some unusual characters as ', or \, you should be fine just by putting your url between tow ':
$ firefox 'YOUR_URL'
This will prevent YOUR_URL content to be evaluated.
Edit, to reflect updated answer:
You can see using echo command how bash expands your parameters.
In your example, bash thinks $ is used to identify a variable (a variable named 4), thus it substitutes $4 with the value of variable 4, which is not defined (thus just removes $4):
$ echo http://www.airportbusiness.com//print/Airport-Business-Magazine/Expo-Returns-to-Vegas/1$41912
http://www.airportbusiness.com//print/Airport-Business-Magazine/Expo-Returns-to-Vegas/11912
$ echo 'http://www.airportbusiness.com//print/Airport-Business-Magazine/Expo-Returns-to-Vegas/1$41912'
http://www.airportbusiness.com//print/Airport-Business-Magazine/Expo-Returns-to-Vegas/1$41912
Never use eval, and always quote your variables. The first argument ist stored in the parameter 1:
firefox "$1" &
This line:
eval website=\${$#}
sets the variable to the last positional parameter, regardless of how many there are.
Change it to:
website=${#: -1}
which is a Bashism, by the way.
Here are a few other Bashisms that accomplish the same thing:
echo "${!#}"
echo "${#:$#}"
echo "${BASH_ARGV[0]}"

Resources