One in awk:
awk '{
for(i=1;i<=length();i++) { # read a line, iterate chars
chr=substr($0,i,1) # get a char
if(chr~/[a-z]/&&(a<=2||((b>=1)&&(c>=1)&&(d>=1)))) { # if in char class a-z
str=str chr # append
a++ # keep count to get
} # ... from each class
if(chr~/[A-Z]/&&(b<=2||((a>=1)&&(c>=1)&&(d>=1)))) {
str=str chr
b++
}
if(chr~/[0-9]/&&(c<=2||((a>=1)&&(b>=1)&&(d>=1)))) {
str=str chr
c++
}
if(chr~/[$%&]/&&(d<=2||((a>=1)&&(b>=1)&&(c>=1)))) {
str=str chr
d++
}
if(length(str)==12) {
print str
exit
}
}
}' /dev/urandom
As the character classes are not the same size and only 3 is allowed from each class before all classes are represented, the random is probably biased.
This one gets accepted characters from /dev/urandom (well, it's readind from a file so where ever you point it to), and 'print`s all matches until you ctrl-c it:
awk '{
for(i=1;i<=length();i++) {
chr=substr($0,i,1)
if(chr~/[a-zA-Z0-9$%&]/)
str=str chr
}
if(length(str)<12)
next
else
for(i=1;i<=length(str)-11;i++) {
sstr=substr(str,i,12)
if(sstr~/[a-z]/ && sstr~/[A-Z]/ && sstr~/[0-9]/ && sstr~/[$%&]/){
print substr(str,i,12)
i+=11
}
}
str=""
}' /dev/urandom
tr -dcremoves unwanted chars, it does not 'force' those. Take a look at this Unix StackExchange question. I thinks this wil work for you:</dev/urandom tr -dc 'A-Za-z0-9!"#$%&'\''()*+,-./:;<=>?@[\]^_{|}~' | head -c 13 ; echo`