Bash em * nix (109)
while ! grep -Pq [A-Z].*[a-z].*[0-9].*[\\W_]<<<$a$a$a$a
do a=`tr -dc !-~</dev/urandom|head -c15`
done
echo $a
Para funcionar corretamente, $anão deve ser definido com uma senha válida, mas não aleatória, com antecedência. Se você deseja incluir a=e uma quebra de linha na frente, são mais três caracteres, mas permite executar a coisa repetidamente. Obviamente, você também pode substituir todas as novas linhas ;por uma única linha que possa ser executada quantas vezes desejar.
Além disso, você deve definir LC_ALL=Cou não variáveis de ambiente específicas do código do idioma ( LANGe LC_CTYPEem particular), pois os intervalos de caracteres dependem da ordem de intercalação igual à ordem ascii.
/dev/urandomé a fonte de bytes aleatórios. !-~é o intervalo de todos os caracteres permitidos, conforme especificado na pergunta. tr -dcremove todos os caracteres não listados em seu próximo argumento. headleva 15 dos caracteres restantes. grepverifica se cada um dos tipos necessários ocorre pelo menos uma vez. Sua entrada consiste em quatro cópias do candidato; portanto, a ordem dos símbolos não importa; portanto, todas as senhas possíveis têm chance de serem selecionadas. O -qto grep suprime a saída.
Por razões desconhecidas, em /dev/randomvez de /dev/urandomlevar idades. Parece que a entropia se esgotou muito rapidamente. Se você cdpara /dev, você pode evitar mais alguns bytes, mas que se sente um pouco como fazer batota.
Python 2 (138)
import re,random
a=''
while not re.search('[A-Z].*[a-z].*[0-9].*[\W_]',a*4):
a=''.join(random.sample(map(chr,range(33,127))*15,15))
print a
Para tornar o código legível, adicionei uma nova linha e recuo após o loop, que não é necessário e que não contei.
Esta é essencialmente a mesma ideia que na versão bash. A fonte aleatória aqui é random.sample, que não repetirá elementos. Para combater esse fato, usamos 15 cópias da lista de cartas permitidas. Dessa forma, todas as combinações ainda podem ocorrer, embora aquelas com letras repetidas ocorram com menos frequência. Mas decido considerar isso um recurso, não um bug, já que a pergunta não exigia igual probabilidade para todas as permutações, apenas a possibilidade.
Python 3 (145)
import re,random
a=''
while not re.search('[A-Z].*[a-z].*[0-9].*[\W_]',a*4):
a=''.join(random.sample(list(map(chr,range(33,127)))*15,15))
print(a)
Uma nova linha e um recuo novamente não são contados. Além de algumas sobrecargas de sintaxe específicas do Python-3, esta é a mesma solução do Python 2.
JavaScript (161)
a=[];for(i=33;i<127;)a.push(s=String.fromCharCode(i++));
while(!/[A-Z].*[a-z].*[0-9].*[\W_]/.test(s+s+s+s))
for(i=0,s="";i<15;++i)s+=a[Math.random()*94|0];alert(s)
Eu adicionei as novas linhas para facilitar a leitura, mas não as contei.
R (114)
s<-""
while(!grepl("[A-Z].*[a-z].*[0-9].*(\\W|_)",paste(rep(s,4),collapse="")))
s<-intToUtf8(sample(33:126,15,T))
s
Quebra de linha e recuo dentro do loop foram adicionados, mas não contados. Se quiser, você pode movê-lo novamente para uma ;linha separada.