No exemplo que você fornece, a única coisa que eval está obtendo é cat , o redirecionamento está acontecendo fora de eval e o arquivo está sendo fornecido como stdin para o comando eval "cat" .
Uma variação é citar todo o comando, incluindo o redirecionamento, usando aspas simples:
$ eval 'cat < "$filename"'
hi
Agora, eval está obtendo a string inteira, incluindo o redirecionamento e o nome da variável, portanto, está fazendo uma expansão de variável e a cotação necessária para o nome do arquivo com espaços. Isso ainda funcionaria.
Outra opção é usar aspas duplas para a string:
$ eval "cat < '$filename'"
hi
Agora, a variável é expandida pelo shell, mas isso ainda funciona, pois as aspas dentro dela mantêm o nome do arquivo juntos. (Observe que isso quebraria se o nome do arquivo contivesse um apóstrofo).
O que não * funciona é isto:
$ eval "cat" "<" "$filename"
Isso é semelhante ao seu exemplo, mas com o < cotado, o redirecionamento não será executado pelo shell externo. eval reunirá os argumentos e o comando resultante será:
cat < my file
O que não funcionará como esperado, já que as aspas em torno de my file já desapareceram ...