Bash hacks that I use to manage my workspaces. The first thing it does is to provide a script that implements a simple notion of modules. This is then used to provide useful modules; and particularly the workspaces module. Workspaces make it easy to setup and switch between different project environments, for example, changing environment variables, paths, and running an emacs server instance per workspace.
Since, bash itself doesn't have a built-in notion of modules a simple one is provided by modules_bootstrap.sh. A module is simply a shell script ending in .sh. The name of module is the filename without the ending .sh.
To use a module it needs to be loaded (imported). There are two basic functions to import modules:
mbimport <module_name> [module_arguments]
mbforce <module_name> [module_arguments]
The first loads the module only if it hasn't been loaded previously,
while the second forces the module to be loaded even if it has
previously been loaded. Both these functions provide a wrapper around
the bash source
command.
While calling source
on a script is simple enough it does have a few
limitations, which the module commands try to address:
-
You need to know where the file is located. Typically support scripts are placed in the same directory as the calling script. To address this the wrapper import functions use the
MODULE_PATH
variable. The path defined by this variable will be searched to find the first filename that matches the module name. Note: of course the limitation of this is that that module names must be unique across all files inMODULE_PATH
. -
The
source
command doesn't provide a mechanism to know if a script has already been loaded. For example, if you have a base script containing general utility functions that are used across many scripts (e.g., list processing functions), you may end up "sourcing" the same file multiple times. To deal with this the import functions set an environment variable named_MB_IMPORTED_<module_name>
that is used to check module has already been loaded. Thembimport
loads a module only once even if the function has been called multiple times. On the other handmbforce
reloads a module regardless of whether it has been previously loaded. -
On exporting. The ability to export variables and functions so that they can be inherited by sub-processes complicates things somewhat. You may or may not want to export variables or functions depending on the scenario. Currently, I don't have got a good way to handle this, although see the comments at the top of the modules_bootstrap.sh script for some discussion. One problem, is that some properties of a shell such as defining aliases and setting up tab completions cannot be inherited by sub-shells. The
mbforce
command is useful for these latter cases where you want to always reload the module.
There is also a support function for adding paths to the modules search path:
mbset_MODULE_PATH <path>
You can of course set the MODULE_PATH
environment variable manually,
but the function tries to be a bit smarter and only adds paths that
are not already present. Note: that executing modules_bootstrap.sh
will automatically add the directory that modules_bootstrap.sh is in
to the MODULE_PATH
.
As an example of how to use these modules my .bashrc
now looks something
like the following:
# Load up the base modules
BASH_DIR=$HOME/local/etc/bash.d
[ "$_MB_IMPORTED_modules_bootstrap" != "1" ] && source $BASH_DIR/modules_bootstrap.sh
# add my local (computer specific) modules to the MODULE_PATH
mbset_MODULE_PATH $HOME/.bash.d
# Import various modules - mostly just to setup my operating environment.
mbimport misc
# Aliases and completions are not inherited so force import every time.
mbforce terminal
# Provide useful workspace emacs function - note: this first loads
# the workspaces module which provides workspace functions in bash.
mbimport workspaces_emacs
# Workspaces function to load a workspace from the current directory.
wksp load_if -p
The following defines some useful modules. The current list of available modules are:
- workspaces - provides bash workspace functionality.
- workspaces_emacs - integrates emacs into a workspace.
- prompts - provides simple prompts (currently only yes/no)
- virtualbox - provides functions to simplify working with virtualbox VMs.
- misc_functions - some miscellaneous functions.
The workspaces module provides functions to setup and control bash-based workspaces. A workspace is simply a directory containing some special files. Workspaces have their own setup and exit scripts as well maintaining their own bash history.
Working within a workspace is implemented by running a sub-bash shell. This ensures that workspaces don't polute each others environment spaces.
Note, that this whole sub-shell thing should be reasonably transparent
to the user and should feel like you are working in a normal
environment. Much of the smarts of this module are about maintaining
this illusion. For example if you call exit
from within a loaded
workspace you want it to act like it was called in a normal
shell. Hence, the workspace functions will actually end up calling
exit
twice, once to exit the workspace and again to exit the base
shell. I'm still undecided if this approach is too heavy handed, but
so far it seems to work reasonably well.
Workspaces use a simple extension concept where other modules can
register hooks (ie. functions) that are run on entering and exiting
workspaces. workspaces_emacs
is such an extension that integrates
named emacs servers for each workspace. It registers an on_enter hook
that is run on entering a workspace and sets up the EDITOR
environment variable to use the wkspe_emacsclient function. It also
registers an on_exit hook that is run on exiting a workspace. This
hook is used to check if the shell exiting the workspace is the last
running instance of that workspace and if so will make sure the emacs
server for that workspace is shutdown.
Following are the special files that are used by this module.
-
Special files/directories:
.workspace
- this sub-directory is created in the directory that is a workspace. This sub-directory contains:-
on_enter.sh
- file that is run on workspace startup. Edit as necessary. -
on_exit.sh
- file that is run on workspace exit. Edit as necessary. -
bash_history
- use this file to maintain the bash history. -
id.<NNNN>
- randomly generated unique identify for workspace. -
pids.tmp
- a temporary file that tracks the open shells for a workspace. -
~/.workspaces
- Contains symlinks to all registered workspaces. This allows for the provision of functions to list and switching betwen workspaces.
-
-
Some environment variables that may be useful from the startup or exit scripts:
WORKSPACE_DIR
- the workspace HOME directory.WORKSPACE_ID
- a workspace identifier, from the workspace id file.
Main user callable command functions:
wksp help
- full list and descriptions of the various options.wksp add
- configure a directory as a workspace.wksp sel
- change to a workspace by selecting from a numbered list.wksp load_if
- If the current directory is a workspace then load it. Useful for adding to .bash_profile to automate loading workspaces.wsls [file]
- shortcut for "wksp ls". "ls" relative toWORKSPACE_DIR
.wscd [directory]
- shortcut for "wksp cd". "cd" relative toWORKSPACE_DIR
.
Most of these functions "echo" their result so should be called with the $(...) pattern.
Operations on string lists:
Functions to manipulate and inspect lists.
Note: 1) a list in this context is just a string that uses the ":" as a separator. These functions are useful for adding to environment variables (e.g, PATH). Note: with the modifying functions, if the list or new string is empty then it simply returns (any) non-empty parts. This avoids creating lists with empty ":" separators. 2) The functions have the string parameter first and the list second. This makes sense because you are typically appending to some existing list that corresponds to a env variable. This variable may or may not be defined (eg., C_INCLUDE_DIR), where as the thing you are trying to append will usually be defined.
-
mf_in_list
Returns true (0) if the string is a member of list and false (1) otherwise.
-
mf_concat <string/list> <string/list>
Simplest version. Concatenates two paths or strings with a ":" separator. Example usage:
PATH=$(mf_concat $PATH "append-path")
-
The following functions are more complex then the simple concat as they provide conditional insert/appending functions.
** mf_cond_insert
Insert the string to the front of the list only if the string is not already a member of the list. Example usage:
PATH=$(mf_cond_insert "p1:p2" "p3")
result: PATH == "p3:p1:p2"
** mf_cond_append
Append the string to the end of the list only if the string is not already a member of the list. Example usage:
PATH=$(mf_cond_insert "p1:p2" "p3")
result: PATH == "p1:p2:p3"
** mf_insert_if_path
Same as mf_cond_insert but first checks that the string is a valid path (ie., file or directory). Example usage:
PATH=$(mf_insert_if "~/include" $PATH)
** mf_append_if
Same as mf_cond_append but first checks that the string is a valid path (ie., file or directory). Example usage:
PATH=$(mf_append_if "~/include" $PATH)
Other function:
-
mf_which : a wrapper around the "which" function that simply returns an empty string if there is no such program.
-
mf_user_loggedin : returns 0 (true) if the user is logged in and 1 (false) otherwise.