main
  1{
  2  lib,
  3  config,
  4  ...
  5}: {
  6  options.programs.bash = {
  7    enable = lib.mkOption {
  8      type = lib.types.bool;
  9      description = ''
 10        Whenever to configure Bash as an interactive shell.
 11      '';
 12      default = true;
 13    };
 14    shellInit = lib.mkOption {
 15      type = lib.types.lines;
 16      description = ''
 17        Shell script code called during bash shell initialisation.
 18      '';
 19      default = "";
 20    };
 21    interactiveShellInit = lib.mkOption {
 22      type = lib.types.lines;
 23      description = ''
 24        Shell script code called during interactive bash shell initialisation.
 25      '';
 26      default = "";
 27    };
 28    shellAliases = lib.mkOption {
 29      type = with lib.types; attrsOf (nullOr (either str path));
 30      description = ''
 31        Set of aliases for bash shell
 32      '';
 33      example = {
 34        ll = "ls -l";
 35        ".." = "cd ..";
 36      };
 37      default = {};
 38    };
 39    shellOptions = lib.mkOption {
 40      type = lib.types.listOf lib.types.str;
 41      default = [
 42        # Append to history file rather than replacing it.
 43        "histappend"
 44
 45        # Extended globbing.
 46        "extglob"
 47        "globstar"
 48
 49        # Warn if closing shell with running jobs.
 50        "checkjobs"
 51      ];
 52      example = [
 53        "extglob"
 54        "-cdspell"
 55      ];
 56      description = ''
 57        Shell options to set. Prefix an option with
 58        "`-`" to unset.
 59      '';
 60    };
 61    history = {
 62      options = lib.mkOption {
 63        type = lib.types.listOf (
 64          lib.types.enum [
 65            "erasedups"
 66            "ignoredups"
 67            "ignorespace"
 68            "ignoreboth"
 69          ]
 70        );
 71        default = [];
 72        description = "Controlling how commands are saved on the history list.";
 73      };
 74      size = lib.mkOption {
 75        type = lib.types.nullOr lib.types.int;
 76        default = 100000;
 77        description = "Number of history lines to keep on memory.";
 78      };
 79      ignore = lib.mkOption {
 80        type = lib.types.listOf lib.types.str;
 81        default = [];
 82        example = [
 83          "ls"
 84          "cd"
 85          "exit"
 86        ];
 87        description = "List of commands that should not be saved to the history list.";
 88      };
 89      file = {
 90        path = lib.mkOption {
 91          type = lib.types.nullOr lib.types.str;
 92          default = null;
 93          description = "Location of the bash history file.";
 94        };
 95        size = lib.mkOption {
 96          type = lib.types.nullOr lib.types.int;
 97          default = 100000;
 98          description = "Number of history lines to keep on file.";
 99        };
100      };
101    };
102  };
103
104  config = let
105    cfg = config.programs.bash;
106
107    historyLines = (
108      lib.concatStringsSep "\n" (
109        lib.mapAttrsToList (n: v: "${n}=${v}") (
110          lib.optionalAttrs (cfg.history.file.size != null) {
111            HISTFILESIZE = toString cfg.history.file.size;
112          }
113          // lib.optionalAttrs (cfg.history.size != null) {
114            HISTSIZE = toString cfg.history.size;
115          }
116          // lib.optionalAttrs (cfg.history.file.path != null) {
117            HISTFILE = ''"${cfg.history.file.path}"'';
118          }
119          // lib.optionalAttrs (cfg.history.options != []) {
120            HISTCONTROL = lib.concatStringsSep ":" cfg.history.options;
121          }
122          // lib.optionalAttrs (cfg.history.ignore != []) {
123            HISTIGNORE = lib.escapeShellArg (lib.concatStringsSep ":" cfg.history.ignore);
124          }
125        )
126        ++ lib.optional (cfg.history.file.path != null) ''mkdir -p "$(dirname "$HISTFILE")"''
127      )
128    );
129
130    shoptsLines = let
131      switch = v:
132        if lib.hasPrefix "-" v
133        then "-u"
134        else "-s";
135    in
136      lib.concatStringsSep "\n" (map (v: "shopt ${switch v} ${lib.removePrefix "-" v}") cfg.shellOptions);
137
138    aliasesLines = builtins.concatStringsSep "\n" (
139      lib.mapAttrsToList (k: v: "alias -- ${k}=${lib.escapeShellArg v}") (
140        lib.filterAttrs (k: v: v != null) cfg.shellAliases
141      )
142    );
143  in {
144    files.".bash_profile".text = ''
145      # load session variables
146      . ${config.environment.loadEnv}
147
148      ${cfg.shellInit}
149
150      # include .bashrc if it exists
151      [[ -f ~/.bashrc ]] && . ~/.bashrc
152    '';
153
154    files.".bashrc".text = ''
155      # Commands that should be applied only for interactive shells.
156      [[ $- == *i* ]] || return
157
158      ${historyLines}
159
160      ${shoptsLines}
161
162      ${aliasesLines}
163
164      ${cfg.interactiveShellInit}
165    '';
166  };
167}