Files
home-manager/modules/programs/bash.nix
Austin Horstman 6ed09becc7 programs/bash: refresh session variables in interactive shells
Interactive non-login Bash shells now apply the current session
variables file before bashrcExtra and initExtra. New terminals pick up
values changed by home-manager switch.

Login shells skip the refresh because .profile already applied it.
Non-interactive shells skip it because sshd can read .bashrc for remote
commands, where startup output can corrupt protocols such as scp and
rsync.

Apply programs.bash.sessionVariables after generic values to preserve
its precedence. Expansions run on every interactive non-login shell, so
warn when direct self-references can change repeatedly. Declarative
search paths remain stable across repeated sourcing.

A profileExtra assignment that collides with a Home Manager-managed
variable is now overwritten in non-login shells. Move such overrides to
initExtra, which runs after the refresh.

targets.genericLinux no longer needs its separate initExtra source.
2026-09-30 21:06:23 -05:00

323 lines
8.7 KiB
Nix

{
config,
lib,
options,
pkgs,
...
}:
let
inherit (lib)
mkIf
mkOption
optionalAttrs
types
;
cfg = config.programs.bash;
writeBashScript =
name: text:
pkgs.writeTextFile {
inherit name text;
checkPhase = ''
${pkgs.stdenv.shellDryRun} "$target"
'';
};
in
{
meta.maintainers = [ lib.maintainers.rycee ];
imports = [
(lib.mkRenamedOptionModule
[ "programs" "bash" "enableAutojump" ]
[
"programs"
"autojump"
"enable"
]
)
];
options = {
programs.bash = {
enable = lib.mkEnableOption "GNU Bourne-Again SHell";
package = lib.mkPackageOption pkgs "bash" {
nullable = true;
default = "bashInteractive";
};
enableCompletion = mkOption {
type = types.bool;
default = true;
description = ''
Whether to enable Bash completion for all interactive Bash shells.
Note, if you use NixOS or nix-darwin and do not have Bash completion
enabled in the system configuration, then make sure to add
```nix
environment.pathsToLink = [ "/share/bash-completion" ];
```
to your system configuration to get completion for system packages.
Note, the legacy {file}`/etc/bash_completion.d` path is
not supported by Home Manager.
'';
};
historySize = mkOption {
type = types.nullOr types.int;
default = 10000;
description = "Number of history lines to keep in memory.";
};
historyFile = mkOption {
type = types.nullOr types.str;
default = null;
description = "Location of the bash history file.";
};
historyFileSize = mkOption {
type = types.nullOr types.int;
default = 100000;
description = "Number of history lines to keep on file.";
};
historyControl = mkOption {
type = types.listOf (
types.enum [
"erasedups"
"ignoredups"
"ignorespace"
"ignoreboth"
]
);
default = [ ];
description = "Controlling how commands are saved on the history list.";
};
historyIgnore = mkOption {
type = types.listOf types.str;
default = [ ];
example = [
"ls"
"cd"
"exit"
];
description = "List of commands that should not be saved to the history list.";
};
shellOptions = mkOption {
type = types.listOf types.str;
default = [
# Append to history file rather than replacing it.
"histappend"
# Extended globbing.
"extglob"
"globstar"
# Warn if closing shell with running jobs.
"checkjobs"
];
example = [
"extglob"
"-cdspell"
];
description = ''
Shell options to set. Prefix an option with
"`-`" to unset.
'';
};
sessionVariables = mkOption {
default = { };
type =
with types;
lazyAttrsOf (
nullOr (oneOf [
str
path
int
float
bool
])
);
example = {
MAILCHECK = 30;
};
description = ''
Environment variables that will be set for the Bash session.
These values are applied again when each interactive non-login shell
starts. Shell expansions run each time, so self-referential values
can change repeatedly.
Setting a value to `null` will skip setting the variable at all, which
may be useful when overriding.
'';
};
shellAliases = mkOption {
default = { };
type = types.attrsOf types.str;
example = {
ll = "ls -l";
".." = "cd ..";
};
description = ''
An attribute set that maps aliases (the top level attribute names in
this option) to command strings or directly to build outputs.
'';
};
profileExtra = mkOption {
default = "";
type = types.lines;
description = ''
Extra commands that should be run when initializing a login
shell.
'';
};
initExtra = mkOption {
default = "";
type = types.lines;
description = ''
Extra commands that should be run when initializing an
interactive shell.
'';
};
bashrcExtra = mkOption {
default = "";
type = types.lines;
description = ''
Extra commands that should be placed in {file}`~/.bashrc`.
Note that these commands will be run even in non-interactive shells.
'';
};
logoutExtra = mkOption {
default = "";
type = types.lines;
description = ''
Extra commands that should be run when logging out of an
interactive shell.
'';
};
};
};
config =
let
aliasesStr = lib.concatStringsSep "\n" (
lib.mapAttrsToList (k: v: "alias -- ${k}=${lib.escapeShellArg v}") cfg.shellAliases
);
shoptsStr =
let
switch = v: if lib.hasPrefix "-" v then "-u" else "-s";
in
lib.concatStringsSep "\n" (map (v: "shopt ${switch v} ${lib.removePrefix "-" v}") cfg.shellOptions);
sessionVarsStr = config.lib.shell.exportAll cfg.sessionVariables;
historyControlStr = (
lib.concatStringsSep "\n" (
lib.mapAttrsToList (n: v: "${n}=${v}") (
optionalAttrs (cfg.historyFileSize != null) {
HISTFILESIZE = toString cfg.historyFileSize;
}
// optionalAttrs (cfg.historySize != null) {
HISTSIZE = toString cfg.historySize;
}
// optionalAttrs (cfg.historyFile != null) {
HISTFILE = ''"${cfg.historyFile}"'';
}
// optionalAttrs (cfg.historyControl != [ ]) {
HISTCONTROL = lib.concatStringsSep ":" cfg.historyControl;
}
// optionalAttrs (cfg.historyIgnore != [ ]) {
HISTIGNORE = lib.escapeShellArg (lib.concatStringsSep ":" cfg.historyIgnore);
}
)
++ lib.optional (cfg.historyFile != null) ''mkdir -p "$(dirname "$HISTFILE")"''
)
);
in
mkIf cfg.enable {
home.packages = lib.mkIf (cfg.package != null) [ cfg.package ];
warnings = config.lib.shell.selfReferenceWarnings {
option = options.programs.bash.sessionVariables;
optionPath = "programs.bash.sessionVariables";
rationale = ''
Home Manager applies these values in each interactive non-login Bash
shell, so self-referential values can change repeatedly.
'';
};
home.file.".bash_profile".source = writeBashScript "bash_profile" ''
# include .profile if it exists
[[ -f ~/.profile ]] && . ~/.profile
# include .bashrc if it exists
[[ -f ~/.bashrc ]] && . ~/.bashrc
'';
# If completion is enabled then make sure it is sourced very early. This
# is to avoid problems if any other initialization code attempts to set up
# completion.
programs.bash.initExtra = mkIf cfg.enableCompletion (
lib.mkOrder 100 ''
if [[ ! -v BASH_COMPLETION_VERSINFO ]]; then
. "${pkgs.bash-completion}/etc/profile.d/bash_completion.sh"
fi
''
);
home.file.".profile".source = writeBashScript "profile" ''
. "${config.home.sessionVariablesPackage}/etc/profile.d/hm-session-vars.sh"
${sessionVarsStr}
${cfg.profileExtra}
'';
home.file.".bashrc".source = writeBashScript "bashrc" ''
# .profile handles login shells. Skip non-interactive shells because
# sshd can read .bashrc for remote commands, where startup output can
# corrupt protocols such as scp and rsync.
${lib.concatStringsSep "\n" (
[
"if [[ $- == *i* ]] && ! shopt -q login_shell; then"
" . \"${config.home.sessionVariablesPackage}/etc/profile.d/hm-session-vars.sh\""
]
# Keep generated exports unindented because continuation lines are data.
++ lib.optional (sessionVarsStr != "") sessionVarsStr
++ [ "fi" ]
)}
${cfg.bashrcExtra}
# Commands that should be applied only for interactive shells.
[[ $- == *i* ]] || return
${historyControlStr}
${shoptsStr}
${aliasesStr}
${cfg.initExtra}
'';
home.file.".bash_logout" = mkIf (cfg.logoutExtra != "") {
source = writeBashScript "bash_logout" cfg.logoutExtra;
};
};
}