Generating variations with bash - bash

Looking for en nicer solution for the next code:
for i in a b c
do
for j in A B C
do
for k in 1 2 3
do
echo "$i$j$k"
done
done
done
Sure is here some simpler solution.

This is a bit simpler
echo {a,b,c}{A,B,C}{1,2,3}
or if want one per line, so
echo {a,b,c}{A,B,C}{1,2,3} | xargs -n1
BTW, you can use the above bracer expansion for example saving keyboard typing for example when need make backup files, like:
cp /some/long/path/And_very-ugly-fileName{,.copy}
will make the /some/long/path/And_very-ugly-fileName.copy without second filename typing.

Use a brace expansion:
echo {a,b,c}{A,B,C}{1,2,3}
This will print all possible combinations between the sets {a,b,c}, {A,B,C}, and {1,2,3}.
Or even simpler, using ranges:
echo {a..c}{A..C}{1..3}
And if you want to print each combination per line, use a for-loop:
for i in {a..c}{A..C}{1..3}; do echo $i; done

Related

Shell - How can I split a line of text into fragments using SED?

I got a output from a command and it is something like this 2048,4096,8192,16384,24576,32768.
I want to split it into 6 different files but only the numbers, not the commas e.g
The initial text: 2048,4096,8192,16384,24576,32768 be split into: 2048 to the file A, 4096 to the file B, 8192 to the file C and so on.
That output follows this rules:
There are always 6 spaces, separated by commas
The numbers are always from 3 to 5 "length" (I don't know the proper English word)
As I told you, commas doesn't interest me because I'm going to do mathematical operations with those numbers
I tried to delete the last X numbers but didn't get a way to "detect" a comma so the operation can stop.
Is it possible using SED?
The following requires on no commands external to a POSIX-compliant shell (such as busybox ash, which you're most likely to be using on Android):
csv=/system/file.csv
IFS=, read a b c d e f <"$csv"
echo "$a" >A
echo "$b" >B
echo "$c" >C
echo "$d" >D
echo "$e" >E
echo "$f" >F
This does assume that the files to be written (A, B, C, D, E and F) are all inside the current working directory. If you want to write them somewhere else, either amend their names, or use cd to change to that other directory.
I'm not sure sed is the right tool for that.
With a simple Bash script:
IFS=',' read -ra val < file.csv
for i in "${val[#]}"; do
echo $i > file$(( ++j ))
done
It writes each values of you csv into file1, file2, etc. :
The read command assigns values from file.csv to array variable val.
Using loop, each value is written to file.
Just make sure you have write permissions in the current directory. If not, change the redirection (eg: > /dirWithWritePermissions/).
This might work for you (GNU sed, bash and parallel):
parallel --xapply echo {1} ">>" part{2} :::: <(sed 's/,/\n/g' file.csv) ::: {1..6}
This "zips" together two files reusing the shorter file as necessary.
N.B. Remember to remove any part* files before applying this command otherwise those files will grow (>> appends).
declare list_of_files=(fileA fileB fileC fileD fileE)
readarray a <<< $(sed 's/,/\n/;{;P;D;}' <<< '2048,4096,8192,16384,24576,32768')
for i in ${a}; do
echo $i > "${list_of_files["$((num++))"]}"
done
Explanation:
s/,/\n/ substitutes every comma with a newline
{ starts a command group
P prints everything in the pattern buffer up to the first newline
D Detetes everything in the pattern buffer up to the first newline and then restarts the current command group
} ends the command group
EDIT:
Let's say you want to copy the information into /system/file but want to have every number in its own row:
$ sed 's/,/\n/;{;P;D;}' < /sys/module/lowmemorykiller/parameters/minfree > /system/file
This will create a new file /system/file that will contain the formatted output.
EDIT: even shorter would: sed 's/,/\n/g', which works just by replacing every comma with a new space (which is done by the g at the end).
Also note that while sed is a nice to tool to use (you gotta love it for its confusing language and commands...), the better and faster way is to use the bash built in read.

Read range of numbers into a for loop

So, I am building a bash script which iterates through folders named by numbers from 1 to 9. The script depends on getting the folder names by user input. My intention is to use a for loop using read input to get a folder name or a range of folder names and then do some stuff.
Example:
Let's assume I want to make a backup with rsync -a of a certain range of folders. Usually I would do:
for p in {1..7}; do
rsync -a $p/* backup.$p
done
The above would recursively backup all content in the directories 1 2 3 4 5 6 and 7 and put them into folders named as 'backup.{index-number}'. It wouldn't catch folders/files with a leading . but that is not important right now.
Now I have a similar loop in an interactive bash script. I am using select and case statements for this task. One of the options in case is this loop and it shall somehow get a range of numbers from user input. This now becomes a problem.
Problem:
If I use read to get the range then it fails when using {1..7} as input. The input is taken literally and the output is just:
{1..7}
I really would like to know why this happens. Let me use a more descriptive example with a simple echo command.
var={1..7} # fails and just outputs {1..7}
for p in $var; do echo $p;done
read var # Same result as above. Just outputs {1..7}
for p in $var; do echo $p;done
for p in {1..7}; do echo $p;done # works fine and outputs the numbers 1-7 seperated with a newline.
I've found a workaround by storing the numbers in an array. The user can then input folder names seperated by a space character like this: 1 2 3 4 5 6 7
read -a var # In this case the output is similar to the 3rd loop above
for p in ${var[#]}; do echo $p; done
This could be a way to go but when backing up 40 folders ranging from 1-40 then adding all the numbers one-by-one completely makes my script redundant. One could find a solution to one of the millennium problems in the same time.
Is there any way to read a range of numbers like {1..9} or could there be another way to get input from terminal into the script so I can iterate through the range within a for-loop?
This sounds like a question for google but I am obviously using the wrong patterns to get a useful answer. Most of similar looking issues on SO refer to brace and parameter expansion issues but this is not exactly the problem I have. However, to me it feels like the answer to this problem is going in a similar direction. I fail to understand why when a for-loop for assigning {1..7} to a variable works but doing the same like var={1..7} doesn't. Plz help -.-
EDIT: My bash version:
$ echo $BASH_VERSION
4.2.25(1)-release
EDIT2: The versatility of a brace expansion is very important to me. A possible solution should include the ability to define as many ranges as possible. Like I would like to be able to choose between backing up just 1 folder or a fixed range between f.ex 4-22 and even multiple options like folders 1,2,5,6-7
Brace expansion is not performed on the right-hand side of a variable, or on parameter expansion. Use a C-style for loop, with the user inputing the upper end of the range if necessary.
read upper
for ((i=1; i<=$upper; i++)); do
To input both a lower and upper bound separated by whitespace
read lower upper
for (i=$lower; i <= $upper; i++)); do
For an arbitrary set of values, just push the burden to the user to generate the appropriate list; don't try to implement your own parser to process something like 1,2,20-22:
while read p; do
rsync -a $p/* backup.$p
done
The input is one value per line, such as
1
2
20
21
22
Even if the user is using the shell, they can call your script with something like
printf '%s\n' 1 2 20..22 | backup.sh
It's easier for the user to generate the list than it is for you to safely parse a string describing the list.
The evil eval
$ var={1..7}
$ for i in $(eval echo $var); do echo $i; done
this also works,
$ var="1 2 {5..9}"
$ for i in $(eval echo $var); do echo $i; done
1
2
5
6
7
8
9
evil eval was a joke, that is, as long as you know what you're evaluating.
Or, with awk
$ echo "1 2 5-9 22-25" |
awk -v RS=' ' '/-/{split($0,a,"-"); while(a[1]<=a[2]) print a[1]++; next}1'
1
2
5
6
7
8
9
22
23
24
25

Bash variable in for loop

I am sure this has been answered before, but I cannot find the solution.
for i in `ls | grep ^t`; do echo $i; done
This gives me the expected results (the files and directories starting with t).
If I want to use a shell variable in the for loop (with the pipe), how do I do this?
This was my start but it does not work.
z="ls | grep ^t"
for i in `$z` ; do echo $i; done
EDIT: I agree this example was not wisely chosen, but what I basically need ishow to use a variable (here $z) in the for loop (for i in $z) which has at least one pipe.
I try to state my question more clearly: How do I need to define my variable (if I want to loop over the output of a command with one or more pipes) and how is the call for the for loop?
To loop through all files starting with t, you should use glob expansion instead of parsing ls output:
$ ls t*
tf tfile
$ ls
abcd tf tfile
$ for i in t*; do echo "$i"; done
tf
tfile
$
Your approach will break in a number of cases, the simplest being when file names contain spaces. There is no need to use any external tool here; t* expands to all files starting with t.
As for your question, you use ls | grep ^t
And another good practice is to use subshell instead of backticks which are more readable and can be nested, use this: $(ls | grep ^t)
You don't use a variable for this. You don't put commands in strings. See Bash FAQ 050 for discussion about why.
You shouldn't be using a for loop to read data line-by-line anyway. See Bash FAR 001 for better ways to do that.
Specifically (though really read that page):
while IFS= read -r line; do
printf '%s\n' "$line"
done < <(some command)

Tricky bash to try to run program with different params

I want to run a program multiple times with different parameters, and then put the results piped into files that use parameters in their names. Here is what I've come up with:
#!/bin/bash
for i in 'seq 1 5';
do
for j in 'seq 1 8';
do
for m in 'seq 1 8';
do
./program -s i -v j -k m ../input_files/input_file1.txt < results_ijm.txt
done
done
done
This doesn't work. It says "no file results_ijm.txt".... I know that - I want it to create this file implicitly.
Otherwise, I also doubt it will assign ijm in the filename correctly - how does it know whether I want the VARIABLES ijm.... or just the characters? It's ambiguous.
You must use variable $i, $j, $m etc.
Better to use ((...)) construct in BASH.
In BASH you can do:
#!/bin/bash
for ((i=1; i<=5; i++)); do
for ((j=1; h<=8; j++)); do
for ((m=1; m<=8; m++)); do
./program -s $i -v $j -k $m ../input_files/input_file1.txt > "results_${i}${j}${m}.txt"
done
done
done
Two problems. As I mentioned in the comments, your arrow is backwards. We want the results of the program to go from stdout to the file so flip that thing around. Second, variables when used gain a dollar sign in front of them... so it won't be ambiguous.
Edited to add: Third thing, use backticks instead of single quotes for seq 1 5 You want the results of that command, not the text "seq 1 5". Thanks #PSkocik
#!/bin/bash
for i in `seq 1 5`;
do
for j in `seq 1 8`;
do
for m in `seq 1 8`;
do
./program -s $i -v $j -k $m ../input_files/input_file1.txt > results_${i}${j}${m}.txt
done
done
done
./program -s i -v j -k m ../input_files/input_file1.txt < results_ijm.txt
I believe the less than symbol should be flipped to greater than, so as to input to file, instead of from file. I've not worked too much with bash, but it seems logical.

For loop with an argument based range

I want to run certain actions on a group of lexicographically named files (01-09 before 10). I have to use a rather old version of FreeBSD (7.3), so I can't use yummies like echo {01..30} or seq -w 1 30.
The only working solution I found is printf "%02d " {1..30}. However, I can't figure out why can't I use $1 and $2 instead of 1 and 30. When I run my script (bash ~/myscript.sh 1 30) printf says {1..30}: invalid number
AFAIK, variables in bash are typeless, so how can't printf accept an integer argument as an integer?
Bash supports C-style for loops:
s=1
e=30
for i in ((i=s; i<e; i++)); do printf "%02d " "$i"; done
The syntax you attempted doesn't work because brace expansion happens before parameter expansion, so when the shell tries to expand {$1..$2}, it's still literally {$1..$2}, not {1..30}.
The answer given by #Kent works because eval goes back to the beginning of the parsing process. I tend to suggest avoiding making habitual use of it, as eval can introduce hard-to-recognize bugs -- if your command were whitelisted to be run by sudo and $1 were, say, '$(rm -rf /; echo 1)', the C-style-for-loop example would safely fail, and the eval example... not so much.
Granted, 95% of the scripts you write may not be accessible to folks executing privilege escalation attacks, but the remaining 5% can really ruin one's day; following good practices 100% of the time avoids being in sloppy habits.
Thus, if one really wants to pass a range of numbers to a single command, the safe thing is to collect them in an array:
a=( )
for i in ((i=s; i<e; i++)); do a+=( "$i" ); done
printf "%02d " "${a[#]}"
I guess you are looking for this trick:
#!/bin/bash
s=1
e=30
printf "%02d " $(eval echo {$s..$e})
Ok, I finally got it!
#!/bin/bash
#BSD-only iteration method
#for day in `jot $1 $2`
for ((day=$1; day<$2; day++))
do
echo $(printf %02d $day)
done
I initially wanted to use the cycle iterator as a "day" in file names, but now I see that in my exact case it's easier to iterate through normal numbers (1,2,3 etc.) and process them into lexicographical ones inside the loop. While using jot, remember that $1 is the numbers amount, and the $2 is the starting point.

Resources