Pense no comando 'source' como na instrução 'include'. Ele pega o conteúdo do argumento e o executa como se tivesse sido executado diretamente. Nesse caso, seu comando é 'source' com um argumento 'run.sh' e run.sh é executado exatamente como se você tivesse digitado o conteúdo de run.sh na linha de comando.
Quando você executa './run.sh', './run.sh' é seu comando e não possui argumentos. Como esse arquivo é de texto sem formatação e não binário, seu shell procura um intérprete no shebang ('#!' Na primeira linha) e encontra '/ bin / bash'. Portanto, seu shell inicia uma nova instância do bash e o conteúdo do run.sh é executado dentro dessa nova instância.
Na primeira instância, quando o bash atinge o comando 'exit', ele é executado exatamente como se você o tivesse digitado na linha de comando. Nos segundos casos, ele é executado no processo do bash, seu shell foi iniciado, portanto, apenas essa instância do bash recebe um comando 'exit'.
Quando você digita uma linha no bash, qualquer coisa antes do primeiro espaço é tratada como um comando e qualquer coisa a seguir é tratada como argumento. O comando '.' é um alias de 'source'. Quando você corre '. run.sh 'o'. ' é um comando por si só, pois é separado dos argumentos por um espaço. Quando você executa './run.sh', seu comando é './run.sh' e '.' faz parte do caminho relativo para run.sh com o '.' representando sua pasta atual.
$- = *i*?