Sign in

    shrc

    Manage environment variables and aliases across bash, zsh, fish and other shells

    shell
    shrc
    env
    alias
    bash
    zsh
    fish
    dotfiles
    cli
    Download zip
    Author
    Version
    0.1.5
    License
    Apache-2.0
    Last updated
    5 days ago
    Downloads
    17

    #shrc

    CI npm Mooncakes License

    Manage environment variables and aliases across every shell you use — bash, zsh, fish, csh/tcsh, nushell, PowerShell and the plain POSIX /bin/sh family — from a single command.

    shrc keeps one managed block in each of your startup files:

    # >>> shrc >>> export EDITOR='vim' alias ll='ls -la' # <<< shrc <<<

    shrc set and shrc rm rewrite that block in place, so running a command twice never duplicates a line, and shrc rm leaves no trace behind.

    #Install

    Pick one of the ways below. Every one of them ends with a working shrc on your PATH.

    curl -LsSf https://raw.githubusercontent.com/tomasky/shrc/master/install.sh | bash

    The installer places the shrc binary, makes it reachable, and runs shrc init once to import the environment variables and aliases your startup files already define into the manifest, so shrc ls starts out showing your real configuration. The startup files themselves are not modified. It prefers a directory that is already on your PATH (~/.local/bin, ~/bin, /usr/local/bin), so the command works right away; otherwise it installs to ~/.shrc/bin, adds that to the startup file for your login shell and prints the one line needed to activate it in the terminal you are in. Sourcing the script updates the current shell too:

    source <(curl -LsSf https://raw.githubusercontent.com/tomasky/shrc/master/install.sh)

    Options: --version <v> (-v, e.g. --version 0.1.2), --install-dir <dir> (-b), --no-modify-path, --no-init, --no-verify. Run install.sh --help for the full list and the environment overrides (SHRC_VERSION, SHRC_INSTALL_DIR, SHRC_REPO, SHRC_DOWNLOAD_BASE, SHRC_NO_MODIFY_PATH, SHRC_NO_INIT).

    #npm

    npm install -g @tomasko/shrc npx @tomasko/shrc ls # or pnpm / yarn / bun, no global install needed

    The npm package is a launcher: it downloads the matching prebuilt binary from GitHub Releases on first run and caches it, so it works under npm, pnpm, yarn, bun and npx with no install script.

    #MoonBit

    moon install kupas/shrc/cmd/shrc

    Installs the published binary into ~/.moon/bin. To use shrc as a MoonBit library instead:

    moon add kupas/shrc

    #Windows

    npm install -g @tomasko/shrc works as-is — the launcher fetches shrc-windows-x86_64.exe on first run. You can also download that .exe from the latest release and put it on your PATH. The installer script recognises Git Bash, MSYS2 and Cygwin, so the one-liner above works there too.

    #Prebuilt binary

    Download the asset for your platform from the latest release, make it executable and put it on your PATH:

    PlatformAsset
    Linux x86-64shrc-linux-x86_64 (static, musl)
    Linux arm64shrc-linux-aarch64 (static, musl)
    macOS Apple siliconshrc-darwin-aarch64
    Windows x86-64shrc-windows-x86_64.exe (MSVC)

    Each asset ships next to a .sha256 checksum file:

    curl -LO https://github.com/tomasky/shrc/releases/latest/download/shrc-linux-x86_64 curl -LO https://github.com/tomasky/shrc/releases/latest/download/shrc-linux-x86_64.sha256 sha256sum -c shrc-linux-x86_64.sha256 install -m 755 shrc-linux-x86_64 ~/.local/bin/shrc

    The Linux builds are linked statically against musl, so they run on any distribution regardless of its glibc version.

    #From source

    git clone https://github.com/tomasky/shrc && cd shrc moon build --target native --release ./_build/native/release/build/cmd/shrc/shrc.exe help

    #Usage

    shrc set EDITOR vim # environment variable, in every shell shrc set NAME=VALUE # same, one argument shrc set -a ll 'ls -la' # an alias instead of a variable shrc set --prepend PATH /opt/bin # add a directory in front shrc set --append PATH /opt/bin # add a directory at the end shrc set --drop PATH /opt/bin # remove a directory shrc set -r EDITOR '$HOME/bin/vi' # store shell syntax; references stay live shrc rm EDITOR # remove every entry named EDITOR shrc rm -a ll # remove only the alias shrc ls # list managed entries shrc ls EDIT # filter by name shrc ls PATH # search paths, one directory per row, in order shrc all # the manifest and every startup file shrc init # import existing definitions (files untouched) shrc refresh # re-scan and sync the manifest with the files shrc version shrc help

    set and rm print the statement that applies the change, so you can make it take effect in the current shell with eval:

    eval "$(shrc set EDITOR vim)" eval "$(shrc rm EDITOR)"

    The printed line is syntax for your login shell ($SHELL): export/alias for bash and zsh, set -gx/alias for fish, setenv for csh/tcsh, $env.X = ... for nushell, and $env:X = ... for PowerShell. A summary goes to standard error, so it never pollutes an eval.

    #Applying changes to the current shell automatically

    shrc itself cannot change its parent shell's environment, so eval is the portable way. If you would rather not type it every time, add a small wrapper to your interactive startup file (~/.bashrc, ~/.zshrc, ~/.config/fish/config.fish); .profile only needs it when it is your login shell's file:

    # bash / zsh / POSIX sh shrc() { case "$1" in set|rm|remove|unset|delete) eval "$(command shrc "$@")" ;; *) command shrc "$@" ;; esac }

    # fish function shrc switch $argv[1] case set rm remove unset delete eval (command shrc $argv) case '*' command shrc $argv end end

    The installer adds this block for you (guarded by a marker, so it is never duplicated). For a manual install, paste it once.

    #Search paths (PATH and friends)

    Variables that hold an ordered list of directories — PATH, PYTHONPATH, LD_LIBRARY_PATH, MANPATH, CLASSPATH and anything else ending in PATH — get list semantics instead of a plain assignment:

    • shrc set PATH /opt/bin prepends the directory; the previous value is kept through $PATH. --append adds at the end instead, --drop removes a directory, and an identical directory is never duplicated.
    • shrc set PATH '/a:/b' (or the PowerShell spelling '/a;/b') replaces the whole list; a value containing $ is stored as live syntax, so shrc set PATH "$HOME/bin:$PATH" keeps expanding per shell.
    • Values are stored canonically with : and rendered with each shell's own separator — PowerShell gets ;: $env:PATH = '$HOME/bin;$env:PATH'.
    • shrc ls PATH prints the list as a table, one directory per row, in search order, instead of one long line.

    #Importing existing configuration

    shrc init (run automatically by the installer) scans the startup files for your detected shells and imports what they already define into the manifest, so shrc ls shows them. The startup files themselves are not touched: their original definitions stay where they are, and only shrc set/shrc rm rewrite the managed block. A definition whose value uses shell syntax shrc cannot translate (command substitutions, backticks, escapes) is skipped. shrc init -n reports what it would import without writing the manifest.

    shrc refresh re-scans and syncs the manifest: new definitions are imported, manually edited values overwrite the stored value (the file is the source of truth for imported entries), and entries no longer present in the files stay tracked until shrc rm. Files are never modified by init/refresh.

    #In-place vs. managed entries

    • An entry adopted from a startup file is imported: it stays in its original place. shrc set NAME value edits that line in place; shrc rm NAME removes it.
    • An entry created by shrc set on a name that does not yet exist is managed: it lives in the >>> shrc >>> block, and shrc set/shrc rm rewrite the block.
    • shrc ls shows both kinds; the manifest marks imported ones with the imported flag (env imported,raw,list PATH ...).

    #How it works

    • The manifest lives in ~/.shrc/entries and is the source of truth for shrc ls and shrc all.
    • A manifest line can carry attributes: raw means the value is shell syntax that stays live (env raw EDITOR $HOME/bin/vi), list means a search path stored with : between directories (env raw,list PATH /opt/bin:$PATH).
    • On every change, shrc renders the manifest for each detected shell and rewrites its managed block.
    • shrc init imports what the startup files already define into the manifest, so shrc ls shows them; the startup files are not modified.
    • A shell is managed when its startup file already exists or it is your login shell; when nothing is found, ~/.profile is used as the portable baseline.

    ShellStartup file
    profile (sh, dash, ...)~/.profile
    bash~/.bashrc (or ~/.bash_profile, ~/.bash_login)
    zsh${ZDOTDIR:-$HOME}/.zshrc (or .zprofile, .zshenv)
    ksh / mksh~/.kshrc / ~/.mkshrc
    fish${XDG_CONFIG_HOME:-$HOME/.config}/fish/config.fish
    csh / tcsh~/.cshrc, ~/.tcshrc, ~/.login
    nushell${XDG_CONFIG_HOME:-$HOME/.config}/nushell/config.nu, env.nu
    PowerShell${XDG_CONFIG_HOME:-$HOME/.config}/powershell/Microsoft.PowerShell_profile.ps1, ~/Documents/PowerShell/...

    #Environment

    VariableEffect
    SHRC_HOMEWhere the manifest lives (default: ~/.shrc)
    SHELLPicks the syntax of the statement printed for eval
    XDG_CONFIG_HOMEBase for the fish, nushell and pwsh startup files
    ZDOTDIRDirectory holding the zsh startup files

    The installer and npm launcher read a few more (SHRC_INSTALL_DIR, SHRC_DOWNLOAD_BASE, SHRC_NO_MODIFY_PATH, SHRC_BINARY, SHRC_CACHE_DIR); run install.sh --help or see npmjs/README.md.

    #Development

    moon check --target native --deny-warn moon test --target native moon fmt moon info

    moon info refreshes pkg.generated.mbti; the tests in shrc_test.mbt (blackbox) and shrc_wbtest.mbt (whitebox) cover quoting, the manifest round trip, block rewriting and shell detection.

    #License

    Apache-2.0

    CliResult

    pub(all) struct CliResult {
    code : Int
    out : String
    err : String
    }

    What a command produced: an exit status, text for standard output and text for standard error.

    Entry

    pub(all) struct Entry {
    kind : EntryKind
    name : String
    value : String
    raw : Bool
    list : Bool
    imported : Bool
    }

    A single managed entry.

    raw marks a value that is shell syntax rather than a literal string: an adopted export PATH="$HOME/bin:$PATH" must keep expanding when the startup file is read, so rendering embeds it in the family's interpolating quotes instead of literal single quotes. list marks a search path — the value is joined by : canonically and rendered with the family's own separator (PowerShell uses ;).

    Entry::new

    fn Entry::new(kind : EntryKind, name : String, value : String) -> Entry

    Build a literal entry.

    Entry::new_full

    fn Entry::new_full(kind : EntryKind, name : String, value : String, raw : Bool, list : Bool, imported? : Bool) -> Entry

    Build an entry with every attribute spelled out.

    Entry::new_raw

    fn Entry::new_raw(kind : EntryKind, name : String, value : String) -> Entry

    Build an entry whose value stays live shell syntax ($VAR, backticks).

    EntryKind

    pub(all) enum EntryKind {
    Env
    Alias
    }

    Whether an entry is an environment variable or an alias.

    FileScan

    pub(all) struct FileScan {
    cleaned : String
    entries : Array[Entry]
    retained : Int
    }

    One scanned startup file.

    Shell

    pub(all) struct Shell {
    name : String
    family : ShellFamily
    rc : String
    }

    One startup file: the shell it belongs to, its syntax family and its path.

    ShellFamily

    pub(all) enum ShellFamily {
    Posix
    Fish
    Csh
    Nu
    PowerShell
    }

    The syntax family a startup file is written in.

    all_candidate_shells

    fn all_candidate_shells() -> Array[Shell]

    Every startup file shrc knows about, existing or not. Used to describe the full surface shrc can manage.

    apply_to_shells

    fn apply_to_shells(entries : Array[Entry], shells : Array[Shell]) -> Array[String]

    Write entries into every startup file in shells.

    Returns one human-readable message per file that could not be written; an empty array means everything succeeded. Imported entries (adopted from the startup files) are left in place and are not rendered into the managed block, so only entries added by shrc set live there.

    block_begin

    let block_begin : String

    The line that opens a managed block.

    block_end

    let block_end : String

    The line that closes a managed block.

    candidate_shells

    fn candidate_shells(home : String, xdg : String, zdotdir : String) -> Array[Shell]

    Every startup file shrc knows about, given the relevant base directories. Pure, so tests can pin the layout.

    canonical_list

    fn canonical_list(name : String, value : String) -> String

    A value adopted from PowerShell, where search paths are joined by ;: stored canonically with : so every family can render it.

    current_eval_family

    fn current_eval_family() -> ShellFamily

    The family of the shell shrc is running under. $SHELL is authoritative where it is set (Unix, Git Bash); otherwise PSModulePath identifies PowerShell, and anything else is treated as POSIX.

    current_shells

    fn current_shells() -> Array[Shell]

    Detect shells against the real filesystem and $SHELL.

    detect_shells

    fn detect_shells(home : String, xdg : String, zdotdir : String, login_shell : String?, exists : (String) -> Bool) -> Array[Shell]

    Detect the startup files shrc should manage.

    A file is managed when it already exists, so every shell the user has configured is covered. The login shell's startup file is always included, creating it when the shell has never been configured, and ~/.profile is the portable fallback when nothing else is found. exists is injected so the decision is testable.

    dispatch

    fn dispatch(args : Array[String]) -> CliResult

    Run one shrc command. args is the argument list without the program name.

    edit_definition_in_text

    fn edit_definition_in_text(text : String, family : ShellFamily, name : String, replacement : String?) -> (String, Bool)

    Replace the definition of name in text with replacement (or remove it when replacement is None). Returns the new text and whether anything changed. Leading indentation of the original line is preserved.

    ensure_dir

    fn ensure_dir(dir : String) -> Result[Unit, String]

    Create dir and any missing parents. @fs.create_dir only makes the last component, and SHRC_HOME is user-supplied, so walk the path.

    entry_eq

    fn entry_eq(a : Entry, b : Entry) -> Bool

    Whether two entries agree on kind, name, value and attributes.

    entry_of_line

    fn entry_of_line(line : String) -> Entry?

    Parse one manifest line, or None when it is blank or malformed.

    entry_to_line

    fn entry_to_line(entry : Entry) -> String

    Serialize one entry as a manifest line: kind<TAB>name<TAB>value, with an extra attribute field before the name when the value is not a plain literal (raw, list, or raw,list).

    escape_field

    fn escape_field(text : String) -> String

    Escape a value so it can live on one tab-separated manifest line.

    eval_family

    fn eval_family(login_shell : String?) -> ShellFamily

    The syntax family to use for the shell that will evaluate shrc's output. $SHELL names the login shell; anything unknown is treated as POSIX.

    family_path_sep

    fn family_path_sep(family : ShellFamily) -> String

    The separator family uses between search-path directories.

    filter_entries

    fn filter_entries(entries : Array[Entry], filter : String) -> Array[Entry]

    Entries whose name equals or contains filter, in manifest order.

    find_entry

    fn find_entry(entries : Array[Entry], wanted : Entry) -> Entry?

    The entry with the same kind and name as wanted, if any.

    format_all

    fn format_all(entries : Array[Entry], shells : Array[Shell]) -> String

    The complete picture: the manifest, then every shell shrc would manage and the block it would receive.

    format_ls

    fn format_ls(entries : Array[Entry]) -> String

    One line per entry, in the order they are stored. A search-path entry is listed as one row per directory, indexed in search order, instead of one long line — so shrc ls PATH shows what the shell will actually search, in order.

    help_text

    fn help_text() -> String

    The help screen.

    home_dir

    fn home_dir() -> String

    The user's home directory. On Windows HOME is often unset, so fall back to USERPROFILE and then to HOMEDRIVE+HOMEPATH; / is the last resort so the pure path helpers stay total.

    install_shell_integration

    fn install_shell_integration() -> Unit

    Append the optional shell integration (auto-eval for set/rm) to every detected startup file that does not already contain it. Safe to call on every run: it is guarded by a marker and never duplicates the block.

    is_search_path_entry

    fn is_search_path_entry(entry : Entry) -> Bool

    Whether an entry should be treated as a search path: an environment variable whose stored flag or name says so.

    is_search_path_name

    fn is_search_path_name(name : String) -> Bool

    Whether name holds a search path. Any name ending in PATH counts, so PATH, PYTHONPATH, LD_LIBRARY_PATH and a project's own MYPATH all behave the same.

    is_valid_alias_name

    fn is_valid_alias_name(name : String) -> Bool

    An alias name: permissive enough for .. and friends, but no whitespace or shell metacharacters, so the rendered alias NAME=... stays a single statement that cannot break out into code.

    is_valid_env_name

    fn is_valid_env_name(name : String) -> Bool

    A POSIX environment variable name: a letter or _, then letters, digits or _. Anything else would not be valid export syntax and could break the startup file it is written into.

    join_path

    fn join_path(parts : Array[String]) -> String

    Join directories back into the stored form.

    kind_eq

    fn kind_eq(a : EntryKind, b : EntryKind) -> Bool

    Whether two kinds are the same. Kept explicit so the enum needs no derived Eq, whose promoted methods would otherwise warn.

    kind_label

    fn kind_label(kind : EntryKind) -> String

    The manifest spelling of a kind.

    load_entries

    fn load_entries() -> Array[Entry]

    Read the manifest. A missing or unreadable manifest is an empty store, not an error: the first shrc set creates it.

    manifest_path

    fn manifest_path() -> String

    Path of the manifest that records every managed entry.

    merge_candidates

    fn merge_candidates(entries : Array[Entry], imported : Array[Entry], candidates : Array[Entry]) -> (Int, Int)

    Merge a file's candidates into entries, appending new ones to imported. Returns (duplicates, conflicts): a duplicate is a line that repeats an existing value, a conflict a line with a different value for an existing name, which keeps the existing value.

    normalize_dir

    fn normalize_dir(dir : String) -> String

    A directory as it is compared: a trailing / is insignificant, / itself is kept.

    parse_entries

    fn parse_entries(text : String) -> Array[Entry]

    Parse a whole manifest.

    parse_kind

    fn parse_kind(text : String) -> EntryKind?

    Parse a manifest kind, accepting the singular and plural spellings.

    path_basename

    fn path_basename(path : String) -> String

    Return the final component of path.

    path_dirname

    fn path_dirname(path : String) -> String

    Return the directory part of path (everything before the last /). Returns "." when there is no separator.

    path_insert

    fn path_insert(value : String, dir : String, at_end : Bool) -> String

    Add dir to a search path, first removing an identical directory so repeating the command never duplicates it. at_end adds it after the existing directories instead of before them.

    path_join

    fn path_join(left : String, right : String) -> String

    Join two path segments with a single /.

    path_remove

    fn path_remove(value : String, dir : String) -> (String, Bool)

    Remove every occurrence of dir from a search path. Returns the new value and whether a directory was removed.

    path_sep

    let path_sep : String

    The separator used in the stored, canonical form.

    quote_csh

    fn quote_csh(value : String) -> String

    Single-quote a value for csh/tcsh. csh expands ! for history even inside single quotes, so it is escaped separately; a ' is closed and reopened around an escaped one.

    quote_csh_live

    fn quote_csh_live(value : String) -> String

    Double-quote a live value for csh/tcsh. ! stays a history escape even inside double quotes, so it is escaped as well.

    quote_fish

    fn quote_fish(value : String) -> String

    Single-quote a value for fish. Inside fish single quotes only \ and ' are special.

    quote_fish_live

    fn quote_fish_live(value : String) -> String

    Double-quote a live value for fish. Interpolation works like a POSIX shell, so the escaping is the same.

    quote_nu

    fn quote_nu(value : String) -> String

    Double-quote a value for nushell. nushell's double-quoted strings do not interpolate $, so only backslash, the quote itself and the control characters need escaping.

    quote_nu_live

    fn quote_nu_live(value : String) -> String

    Quote a live value for nushell: an interpolating $"..." string, where $VAR expands at load time.

    quote_posix

    fn quote_posix(value : String) -> String

    Single-quote a value for a POSIX shell (' becomes '\'').

    quote_posix_live

    fn quote_posix_live(value : String) -> String

    Double-quote a value for a POSIX shell while leaving $ and backticks live, so an adopted PATH keeps expanding. Only \ and " are escaped.

    quote_ps

    fn quote_ps(value : String) -> String

    Single-quote a value for PowerShell. A single quote is doubled.

    quote_ps_live

    fn quote_ps_live(value : String) -> String

    Double-quote a live value for PowerShell, where $VAR expands inside double quotes. The backtick is PowerShell's escape character.

    refs_translatable

    fn refs_translatable(text : String) -> Bool

    Whether every $ in text is a variable reference shrc can translate between shells. Command substitutions and backticks cannot be translated, so a value containing them is not adopted.

    remove_by_name

    fn remove_by_name(entries : Array[Entry], name : String) -> (Array[Entry], Array[Entry])

    Split entries into the ones that survive and the ones matching name.

    render_apply

    fn render_apply(family : ShellFamily, entry : Entry) -> String

    The statement that applies entry to the shell that evaluates it.

    render_entry

    fn render_entry(family : ShellFamily, entry : Entry) -> String

    Render one entry in the syntax of family.

    render_remove

    fn render_remove(family : ShellFamily, entry : Entry) -> String

    The statement that removes entry from the shell that evaluates it.

    same_dir

    fn same_dir(a : String, b : String) -> Bool

    Whether two directories name the same place.

    save_entries

    fn save_entries(entries : Array[Entry]) -> Result[Unit, String]

    Persist the manifest, creating SHRC_HOME when needed.

    scan_file

    fn scan_file(family : ShellFamily, text : String) -> FileScan

    Scan one startup file: every line that defines an entry becomes a candidate, and the file text comes back with the adopted lines removed — or, where removing one would empty a conditional body or fan out a helper, replaced by a no-op (:) so the file stays valid.

    A definition inside a function body belongs to code that runs later and is never touched. Lines inside a managed block are read as candidates too — they let shrc init rebuild a manifest that was lost — but they are never removed, because the block is rewritten from the manifest anyway.

    search_path_vars

    fn search_path_vars() -> Array[String]

    The search-path variables that are not simply named *PATH. Names ending in PATH are recognised without being listed.

    self_ref

    fn self_ref(name : String) -> String

    The canonical self-reference of a variable: $NAME — so a search path shrc creates keeps whatever the shell already had.

    serialize_entries

    fn serialize_entries(entries : Array[Entry]) -> String

    Render the whole manifest, one entry per line.

    shell_integration_block

    fn shell_integration_block(family : ShellFamily) -> String

    A block that defines a shrc shell function which applies set/rm to the current shell automatically. Returns an empty string for families where no integration is provided.

    shrc_home

    fn shrc_home() -> String

    The shrc home directory. Honours SHRC_HOME, otherwise $HOME/.shrc.

    shrc_version

    let shrc_version : String

    The version of this shrc build.

    This is the single source for shrc version. Keep it in step with moon.mod and npmjs/package.json when a release is cut.

    sourced_files

    fn sourced_files(family : ShellFamily, text : String) -> Array[String]

    The files a startup file loads with ./source, resolved under the home directory. They are scanned like the startup file itself, so definitions kept in a companion like ~/.bash_aliases are managed too.

    split_path

    fn split_path(value : String) -> Array[String]

    Split a stored search-path value into its directories, in order. An empty value has no directories.

    sync_block

    fn sync_block(text : String, family : ShellFamily, entries : Array[Entry]) -> String

    Return text with its managed block replaced by the one for entries.

    A file without a block gets one appended; an empty entry list removes the block entirely, so shrc rm leaves no trace. Duplicate or unbalanced markers — what a hand edit can leave behind — are collapsed too: the first complete block is rewritten in place and any other marker or block is dropped, so a file can never accumulate two shrc blocks, and a stray opening marker can never swallow the lines around it.

    translate_refs

    fn translate_refs(family : ShellFamily, value : String) -> String

    Rewrite every variable reference in value for the syntax of family. Values are stored canonically ($NAME), so a value adopted from nushell or PowerShell renders correctly in every other family.

    unescape_field

    fn unescape_field(text : String) -> String

    Inverse of escape_field.

    upsert_entry

    fn upsert_entry(entries : Array[Entry], entry : Entry) -> Array[Entry]

    Insert entry, replacing any entry with the same kind and name.

    xdg_config_home

    fn xdg_config_home() -> String

    $XDG_CONFIG_HOME, or $HOME/.config when it is unset.

    zdotdir

    fn zdotdir() -> String

    The directory zsh keeps its dotfiles in ($ZDOTDIR), or $HOME.