Two things, first this is my first question in this forum and I do apologise if the formating is all over the place. Second I have not written that many bash scripts, and it tend to be quite a long time between the scripts I produce.
That said, here is my question.
Is it possible to do something like this in bash (Clear array $array contains):
$array=()
Basically this is what I would like to do. I have a variable with array variable names in it:
array1=()
array2=()
arrayList="array1 array2"
# In a function far far away
for array in $arrayList
do
eval arr=("\"\${$array[#]\"")
for index in ${!arr[#]}
do
echo "${arr[$index]}"
done
# Here is the big "?", I like to clear the array that $array refers to.
$array=()
done
My arrays contain strings that include "" (space) and this is why I use the eval statement. Not sure it's needed but at least it's working. The script is more or less working as I want it too, but I need to clear the arrays in the $arrayList, and I rather not hardcode it somewhere, even though that would be easy.
Probably the simplest thing to do is just unset them. An unset variable will act identically to an empty array in most contexts, and unset $array ought to work fine.
You can't do $foo=bar ever -- that isn't how indirect assignments in bash work. Unfortunately, while being able to do indirect array assignments is an available feature in ksh93, it isn't a formally documented available feature in bash.
Quoting BashFAQ #6 (which should be read in full if you're interested in knowing more about using indirect variables in general):
We are not aware of any trick that can duplicate that functionality in POSIX or Bourne shells (short of using eval, which is extremely difficult to do securely). Bash can almost do it -- some indirect array tricks work, and others do not, and we do not know whether the syntax involved will remain stable in future releases. So, consider this a use at your own risk hack.
# Bash -- trick #1. Seems to work in bash 2 and up.
realarray=(...) ref=realarray; index=2
tmp="$ref[$index]"
echo "${!tmp}" # gives array element [2]
# Bash -- trick #2. Seems to work in bash 3 and up.
# Does NOT work in bash 2.05b.
tmp="$ref[#]"
printf "<%s> " "${!tmp}"; echo # Iterate whole array.
However, clearing is simpler, as unset $array will work fine.
array=()
It clears the array. I guess that is what you wanted..
Related
I'm trying to create a for-loop to go through all the items from an array, and add the items to a string. The tags are given as a single string with format "tag1 tag2 tag3", and the tagging parameter can be given as many times as I want with the single command with syntax "-tag tag1 -tag -tag2 -tag tag3". I'm unable to create a for loop for the job, and I'm a little confused what is wrong with my code.
TAGS="asd fgh jkl zxc bnm" # Amount of tags varies, but there is always at least one
ARRAY=($TAGS)
TAGSTOBEADDED=""
for i in "$ARRAY[#]"
do
STRINGTOBEADDED="-tag ${ARRAY[$i]}"
$TAGSTOBEADDED=$TAGSTOBEADDED+$STRINGTOBEADDED
done
command $TAGSTOBEADDED
First, your array sintax is wrong as #oguz ismail said. To iter through array items you shold use this:
for i in "${ARRAY[#]}"; { echo $i;}
Second $TAGSTOBEADDED=$TAGSTOBEADDED+$STRINGTOBEADDED this is also fail.
Variables are set like so var="$var 123" you don't need $ in front of var name if you want to change it. Back to code. In this example you dont even need an array, just use TAGS var(without ""):
for i in $TAGS; { TAGSTOBEADDED+="-tag $i"; }
First: avoid storing lists of things in space-delimited strings (as you're currently doing with TAGS and TAGSTOBEADDED) -- there are a bunch of things that can go wrong if they have any "funny" characters (or if IFS gets changed). Use an array instead. Storing them as a string and then converting doesn't help; all of the same potential problems apply during the conversion.
I also recommend using lower- or mixed-case variable names in scripts, since there are a bunch of all-caps names with special meanings, and accidentally using one of those for something else can have weird effects. So, to define the array of tags, I'd just use this:
tags=(asd fgh jkl zxc bnm)
You also have a number of syntax errors in the script. In this line:
for i in "$ARRAY[#]"
... the shell will try to expand $ARRAY as a plain variable (not an array), and then treat "[#]" as just some unrelated characters that go after it. You need braces around the variable refence (like "${ARRAY[#]}") any time you're doing anything nontrivial with a variable reference. BTW, this idiom -- including double-quotes, braces, square-brackets and at-sign -- is what you almost always want when getting the contents of an array.
In this line:
STRINGTOBEADDED="-tag ${ARRAY[$i]}"
$i will expand to one of the array elements, not its index. That is, it'll expand to something like:
STRINGTOBEADDED="-tag ${ARRAY[asd]}"
...which doesn't make any sense. You just want
STRINGTOBEADDED="-tag $i"
...except you don't want that either, because (as I said before) storing lists of things space-delimited in a string is a bad idea. But I'll get to that because fixing it will involve the next line:
$TAGSTOBEADDED=$TAGSTOBEADDED+$STRINGTOBEADDED
There are two problems here: you don't want a dollar sign on the variable being assigned to ($varname gets the value of a variable; anytime you're setting it, don't use the $). Also the + isn't needed to add strings, you just stick them end to end. Well, you'd need to add a space in between, something like one of these:
TAGSTOBEADDED=$TAGSTOBEADDED" "$STRINGTOBEADDED
TAGSTOBEADDED="$TAGSTOBEADDED $STRINGTOBEADDED"
(Generally, you should have double-quotes around all variable references; on the right side of a plain assignment is one of the few places it's safe to leave them unquoted, but I tend to prefer to just double-quote always rather than try to remember all of the exceptions about where it's safe and where it isn't. Plus, quoting just the space looks weird.)
But you don't want to do that either, because (again) space-delimited strings are a bad way to do things. Use an array. So before the loop, create an empty array instead of an empty string:
tagstobeadded=()
...and then inside the loop, append to it with +=( ):
tagstobeadded+=(-tag "$i")
...and then at the end, use it with all the appropriate quotes, braces, etc:
command "${tagstobeadded[#]}"
So, with all of these changes, here's what I'd recommend:
tags=(asd fgh jkl zxc bnm)
tagstobeadded=()
for i in "${tags[#]}"
do
tagstobeadded+=(-tag "$i")
done
command "${tagstobeadded[#]}"
Brace expansion takes a pattern and expands it. For example:
sp{el,il,al}l
Expands to:
spell spill spall
Is there an algorithm (potentially with a JavaScript implementation) to do the reverse in a way that minimizes the constructed string?
i.e., take in an array [spell spill spall] and return a string "sp{e,i,a}ll"
Minimizing the resulting string can be done in many different ways, but since you mention Bash, I'll choose the Bash way which is not the most optimized one.
Yes, there is a Bash way! Bash creators have included it as the readline command complete-into-braces. When using Bash interactively, if you hit Meta{ (which is either Alt{ or Esc-then-{ on my machine), all possible completions are grouped into one single brace expansion.
$ echo /usr/
bin/ games/ include/ lib/ local/ sbin/ share/ src/
$ echo /usr/{bin,games,include,l{ib,ocal},s{bin,hare,rc}}
Above, the first time I hit Tab to show all possible completions, and the second time I hit Alt{.
Back to your question: you are looking for an algorithm. Obviously you may find something in Bash source code. The function you are looking for is really_munge_braces() in bracecomp.c
As requested in the original question, node-brace-compression contains a JavaScript implementation. E.g.
var compress = require('brace-compression');
var data = [
'foo-1',
'foo-2',
'foo-3'
];
console.log(compress(data));
// => "foo-{1..3}"
I'm trying to write a basic find command for a assignment (without using find). Right now I have an array of files I want to exec something on. The syntax would look like this:
-exec /bin/mv {} ~/.TRASH
And I have an array called current that holds all of the files. My array only holds /bin/mv, {}, and ~/.TRASH (since I shift the -exec out) and are in an array called arguments.
I need it so that every file gets passed into {} and exec is called on it.
I'm thinking I should use sed to replace the contents of {} like this (within a for loop):
for i in "${current[#]}"; do
sed "s#$i#{}"
#exec stuff?
done
How do I exec the other arguments though?
You can something like this:
cmd='-exec /bin/mv {} ~/.TRASH'
current=(test1.txt test2.txt)
for f in "${current[#]}"; do
eval $(sed "s/{}/$f/;s/-exec //" <<< "$cmd")
done
Be very careful with eval command though as it can do nasty things if input comes from untrusted sources.
Here is an attempt to avoid eval (thanks to #gniourf_gniourf for his comments):
current=( test1.txt test2.txt )
arguments=( "/bin/mv" "{}" ~/.TRASH )
for f in "${current[#]}"; do
"${arguments[#]/\{\}/$f}"
done
Your are lucky that your design is not too bad, that your arguments are in an array.
But you certainly don't want to use eval.
So, if I understand correctly, you have an array of files:
current=( [0]='/path/to/file'1 [1]='/path/to/file2' ... )
and an array of arguments:
arguments=( [0]='/bin/mv' [1]='{}' [2]='/home/alex/.TRASH' )
Note that you don't have the tilde here, since Bash already expanded it.
To perform what you want:
for i in "${current[#]}"; do
( "${arguments[#]//'{}'/"$i"}" )
done
Observe the quotes.
This will replace all the occurrences of {} in the fields of arguments by the expansion of $i, i.e., by the filename1, and execute this expansion. Note that each field of the array will be expanded to one argument (thanks to the quotes), so that all this is really safe regarding spaces, glob characters, etc. This is really the safest and most correct way to proceed. Every solution using eval is potentially dangerous and broken (unless some special quotings is used, e.g., with printf '%q', but this would make the method uselessly awkward). By the way, using sed is also broken in at least two ways.
Note that I enclosed the expansion in a subshell, so that it's impossible for the user to interfere with your script. Without this, and depending on how your full script is written, it's very easy to make your script break by (maliciously) changing some variables stuff or cd-ing somewhere else. Running your argument in a subshell, or in a separate process (e.g., separate instance of bash or sh—but this would add extra overhead) is really mandatory for obvious security reasons!
Note that with your script, user has a direct access to all the Bash builtins (this is a huge pro), compared to some more standard find versions2!
1 Note that POSIX clearly specifies that this behavior is implementation-defined:
If a utility_name or argument string contains the two characters "{}", but not just the two characters "{}", it is implementation-defined whether find replaces those two characters or uses the string without change.
In our case, we chose to replace all occurrences of {} with the filename. This is the same behavior as, e.g., GNU find. From man find:
The string {} is replaced by the current file name being processed everywhere it occurs in the arguments to the command, not just in arguments where it is alone, as in some versions of find.
2 POSIX also specifies that calling builtins is not defined:
If the utility_name names any of the special built-in utilities (see Special Built-In Utilities), the results are undefined.
In your case, it's well defined!
I think that trying to implement (in pure Bash) a find command is a wonderful exercise that should teach you a lot… especially if you get relevant feedback. I'd be happy to review your code!
I have an array which contains
commName[0]="ls"
commName[1]="date"
commName[2]="crontab"
commName[3]="uname"
commName[4]="hostname"
Now the array doesn't always contain these. Sometimes it can have more indices sometimes less. And the values are not always ls,date,... They can be different. Bottom line, I don't know the size nor the values of the array when I'm coding.
Every array value ls,date,... has its own unique address. So for example, ls would have /home/test/ and date would have /home/test/test2/ etc... These addresses need to be stored into a variable which will be used later on in the code. So I should have following variables according to the given array
$lsAddress
$dateAddress
$crontabAddress
$unameAddress
$hostnameAddress
Therefore, I need a way to make these variables (have in mind that I don't know ls,date,uname,....)
My approach was this
for ((j=0 ; j<${#commName[#]} ; j++))
do
set commName[$j]Nick="hi"
echo $(${commName[$j]}Nick)
done
What I expected this to do was to create new variables for every index of the array and set them equal to hi (just for test purposes) and then access those new variables.
Also, The new created variables Must be accessible anywhere. So, I can't have a temporary variable that keeps getting replaced.
However, this method isn't working... Is there any other way I can do this?
Use eval. Try this:
for ((j=0 ; j<${#commName[#]} ; j++))
do
param=`echo ${commName[$j]}Nick`
eval "$param=hi1"
eval "echo \$$param"
done
Use two parallel arrays, so that the entry in the command array matches with the corresponding entry in the address array.
commName[0]="ls"
commName[1]="date"
commName[2]="crontab"
commName[3]="uname"
commName[4]="hostname"
commAddress[0]="/home/test/" # ls
commAddress[1]="/home/test/test2" # date
# etc
Then, when you have a particular value of i, you know that ${commName[i]} and ${commAddress[i]} go together.
I recommend the two arrays, but you might also consider using bash's indirect parameter expansion instead.
$ commName[0]="ls"
$ lsAddress="/home/test"
$ name="${commName[0]}Address"
$ echo "${!name}"
/home/test
I'm working on a script that has a number of functions in place which pull data from a few different arrays. We hope to keep the arrays individualized for reporting purposes. The information in the arrays does not change and the only thing different between each function is which array name is being used. Since all of the functions have 98% the same content I'm trying to pull them into 1 single array for simplified management.
The issue I'm facing though is that I'm not able to figure out the correct syntax to obtain the length of an array based on the array title that is passed in the function argument. I can't post the actual script, but here is a mock up that details a simplified version of what I'm testing with. I believe if we can get it working using the mock script below I can transfer the needed changes to the actual script.
array1=(
"item1 123"
"item2 456"
)
array2=(
"stockA qwe"
"stockB asd"
"stockC zxc"
)
test() {
local ref=${1}[#]
IFS=$'\n'; for i in ${!ref}; do echo $i ; done
}
test array1
test array2
The script above so far will echo the content of each array line based on argument 1 when the function and it's argument is called; which is working as needed. I've tried many different combinations such as len=${#${1}[#]} but I always receive a "bad substitution" error. The functions I mention before have while loops and for statements that use the array length to know when to stop, so being able to pull that information really ties it all together. What I'm hoping for is something like the flow below
I plan to continue my research on this, but thank you for any help and knowledge that can be provided!
-Cyanide
I think the only solution is to create a copy of the array, then take the length of that array:
local ref=${1}[#]
copy=( "${!ref}" )
len=${#copy[#]}
Since bash does not allow chaining of the parameter expansion operators, I know of no shorter way to use both ${#...} and ${!...} on the same line.