BASH Accessing the next 'field' in command line without a loop - bash

This may seem like a silly question, but I've looked high and low for a solution and have not come up with one. I have a script that accepts a number of arguments and I simply wish to know how to have one argument and access the next immediate argument. I've tried stuff like $i+1 with no avail. This next argument will end up holding a destination directory, so I need to be able to access the contents. Also note that I am getting the argument position through a counter variable iterated in a loop, which is why I can't simply say $2 or something. I apologize for such a mundane question.

$ bash -c 'foo=1 ; echo ${#:$((foo+2)):1}' a b c d e f g
d

If i is a variable containing an integer, and you want the (i+1)st argument, you can do:
eval arg=\$$(( i + 1 ))
Now arg contains the argument you want

It might not be applicable in your case, but if you are accessing the arguments sequentially, you could use the inbuilt shift command to shift through the arguments.
Doing shift with no argument will set $1 to $2, $2 to $3 and so on, discarding the old value of $1. Giving a numerical argument to shift will shift that many steps.

Related

Use default value if no arguments are set except the first one in a bash script

From "Process all arguments except the first one (in a bash script)" I have learned how to get all arguments except the first one. Is it also possible to substitute null with another value, so I can define a default value?
I've tried the following, but I think I don't get some little detail about the syntax:
DEFAULTPARAM="up"
echo "${#:2-$DEFAULTPARAM}"
Here are my test cases:
$ script.sh firstparam another more
another more
$ script.sh firstparam another
another
$ script.sh firstparam
up
You cannot combine 2 expressions like that in bash. You can get all arguments from position 2 into a separate variable and then check/get default value:
defaultParam="up"
from2nd="${#:2}" # all arguments from position 2
echo "${from2nd:-$defaultParam}" # return default value if from2nd is empty
PS: It is recommended to avoid all caps variable names in your bash script.
Testing:
./script.sh firstparam
up
./script.sh firstparam second
second
./script.sh firstparam second third
second third
bash doesn't generally allow combining different types of special parameter expansions; this is one of the combos that doesn't work. I think the easiest way to get this effect is to do an explicit test to decide whether to use the default value. Exactly how to do this depends on the larger situation, but probably something like this:
#!/bin/bash
DEFAULTPARAM="up"
if (( $# >= 2 )); then
allbutfirstarg=("${#:2}")
else
allbutfirstarg=("$DEFAULTPARAM")
fi
echo "${allbutfirstarg[#]}"

How to read argument value inside for loop range for shell scripting [duplicate]

I'm working on getting accustomed to shell scripting and ran across a behavior I found interesting and unexplained. In the following code the first for loop will execute correctly but the second will not.
declare letters=(a b c d e f g)
for i in {0..7}; do
echo ${letters[i]}
done
for i in {0..${#letters[*]}}; do
echo ${letters[i]}
done
The second for loop results in the following error:
syntax error: operand expected (error token is "{0..7}")
What confuses me is that ${#letters[*]} is clearly getting evaluated, correctly, to the number 7. But despite this the code fails even though we just saw that the same loop with {0..7} works perfectly fine.
What is the reason for this?
I am running OS X 10.12.2, GNU bash version 3.2.57.
The bracket expansion happens before parameter expansion (see EXPANSIONS in man bash), therefore it works for literals only. In other words, you can't use brace expansion with variables.
You can use a C-style loop:
for ((i=0; i<${#letters[#]}; i++)) ; do
echo ${letters[i]}
done
or an external command like seq:
for i in $(seq 1 ${#letters[#]}) ; do
echo ${letters[i-1]}
done
But you usually don't need the indices, instead one loops over the elements themselves, see #TomFenech's answer below. He also shows another way of getting the list of indices.
Note that it should be {0..6}, not 7.
Brace expansion occurs before parameter expansion, so you can't use a variable as part of a range.
Expand the array into a list of values:
for letter in "${letters[#]}"; do
echo "$letter"
done
Or, expand the indices of the array into a list:
for i in ${!letters[#]}; do
echo "${letters[i]}"
done
As mentioned in the comments (thanks), these two approaches also accommodate sparse arrays; you can't always assume that an array defines a value for every index between 0 and ${#letters[#]}.

Iteratively pass one file from a collection of files as command line parameters in shell script

I have x files: A, B, C... What I need to do is pass each of these files as the first command line argument to a python file and pass the others as the second command line argument until all files have been passed as $1 once. For example, on the first iteration A is $1 and B,C... is $2. On the second iteration, B is $1 and A,C... is $2. I've read about the shift command in shell but am not very sure if it will work in my case (I'm also relatively new to shell scripting). Also, is there a limit to the number of command line arguments I can pass to my python script? I would also like to create a variable to hold the list of file names before iterating through my files. Thank you!
Bash has arrays, and supports array slicing via ${array[#]:start:end} syntax, where start and end are optional indices. That's enough to get the job done.
#!/bin/bash
# Store the master list of file names in an array called $files.
files=("$#")
for ((i = 0; i < ${#files[#]}; ++i)); do
# Store the single item in $file and the rest in an array $others.
file=${files[i]}
others=("${files[#]:0:i}" "${files[#]:i+1}")
# Run command.py. Use ${others[*]} to concatenate all the file names into one long
# string, and override $IFS so they're joined with commas.
(IFS=','; command.py "${files[i]}" "${others[*]}")
done

Remove last argument from argument list of shell script (bash)

This question concerns a bash script that is run in automator osx. I am using automator actions to get and filter a bunch of file references from the finder. Then I append to that list the name of the parent folder, also via an automator action. Automator then feeds these arguments to an action called "run shell script". I am not sure exactly how automator invokes the script but the argument list looks like this when echoed with: echo "$#"
/Volumes/G-Raid/Online/WAV_TEST/Testbok 50/01/01000 43-001.wav /Volumes/G-Raid/Online/WAV_TEST/Testbok 50/02/02000 43-002.wav /Volumes/G-Raid/Online/WAV_TEST/Testbok 50/03/03000 43-003.wav /Volumes/G-Raid/Online/WAV_TEST/Testbok 50
In this case path to 3 files and a folder.
In the shell script I launch an application called ripcheckc* with the args passed from automator minus the last argument(the folder) in the list.
I use this to remove the last argument:
_args=( "$#" )
unset _args[${#_args[#]}-1]
And this is echo $_args:
/Volumes/G-Raid/Online/WAV_TEST/Testbok 50/01/01000 43-001.wav /Volumes/G-Raid/Online/WAV_TEST/Testbok 50/02/02000 43-002.wav /Volumes/G-Raid/Online/WAV_TEST/Testbok 50/03/03000 43-003.wav
Same as before but without the folder.
Now, if I run ripcheckc with "$#" as argument it works (but fails later on because of that last path in the argument list) If I use ${_args[#]} the application will just abort silently. When I echo $# and _args the output looks identical except for the last argument.
My question is - what is the difference between $# and $_args that make the first valid input and the second not?
*The application is ripcheckc
I hope my question makes sense.
EDIT: Solved.
I have used this bash one-liner before
set -- "${#:1:$(($#-1))}"
It sets the argument list to the current argument list, less the last argument.
How it works:
$# is the number of arguments
$((...)) is an arithmetic expression, so $(($#-1)) is one less than the number of arguments.
${variable:position:count} is a substring expression: it extracts count characters from variable starting at position. In the special case where variable is #, which means the argument list, it extracts count arguments from the list beginning at position. Here, position is 1 for the first argument and count is one less than the number of arguments worked out previously.
set -- arg1...argn sets the argument list to the given arguments
So the end result is that the argument list is replaced with a new list, where the new list is the original list except for the last argument.
Assuming that you already have an array, you can say:
unset "array[${#array[#]}-1]"
For example, if your script contains:
array=( "$#" )
unset "array[${#array[#]}-1]" # Removes last element -- also see: help unset
for i in "${array[#]}"; do
echo "$i"
done
invoking it with: bash scriptname foo bar baz produces:
foo
bar
You can also get all but the last argument with
"${#:0:$#}"
which, honestly, is a little sketchy, since it seems to be (ab)using the fact that arguments are numbered starting with 1, not 0.
Update: This only works due to a bug (fixed in 4.1.2 at the latest) in handling $#. It works in version 3.2.
set -- "${#:1:$#-1}"
sets the parameter list to first up to penultimate, removing the last one

Open file in bash script

I've got a bash script accepting several files as input which are mixed with various script's options, for example:
bristat -p log1.log -m lo2.log log3.log -u
I created an array where i save all the index where i can find files in the script's call, so in this case it would be an arrat of 3 elements where
arr_pos[0] = 2
arr_pos[1] = 4
arr_pos[3] = 5
Later in the script I must call "head" and "grep" in those files and i tried this way
head -n 1 ${arr_pos[0]}
but i get this error non runtime
head: cannot open `2' for reading: No such file or directory
I tried various parenthesis combinations, but I can't find which one is correct.
The problem here is that ${arr_pos[0]} stores the index in which you have the file name, not the file name itself -- so you can't simply head it. The array storing your arguments is given by $#.
A possible way to access the data you want is:
#! /bin/bash
declare -a arr_pos=(2 4 5)
echo ${#:${arr_pos[0]}:1}
Output:
log1.log
The expansion ${#:${arr_pos[0]}:1} means you're taking the values ranging from the index ${arr_pos[0]} in the array $#, to the element of index ${arr_pos[0]} + 1 in the same array $#.
Another way to do so, as pointed by #flaschenpost, is to eval the index preceded by $, so that you'd be accessing the array of arguments. Although it works very well, it may be risky depending on who is going to run your script -- as they may add commands in the argument line.
Anyway, you may should try to loop through the entire array of arguments by the beginning of the script, hashing the values you find, so that you won't be in trouble while trying to fetch each value later. You may loop, using a for + case ... esac, and store the values in associative arrays.
I think eval is what you need.
#!/bin/bash
arr_pos[0]=2;
arr_pos[1]=4;
arr_pos[2]=5;
eval "cat \$${arr_pos[1]}"
For me that works.

Resources