mcsh is a consolidated, modernised fusion of tcsh (by the tcsh-org maintainers, originally by Christos Zoulas and the tcsh community, building on the work of Ken Greer, Mike Ellis, and Paul Placeway) and etcsh (by Krush206) into a single, polished, fully compatible reincarnation of the Berkeley C Shell. Both upstream projects are the authoritative sources of the underlying shell engine and etcsh language extensions respectively — mcsh exists to merge and maintain them.
The installed program is mcsh(1). Everywhere in this repository, in the
binary, and in the manual page, the shell identifies itself as
Modern C Shell — not as tcsh, etcsh, or csh.
mcsh is a drop-in replacement for tcsh and csh:
| Compatibility item | Behaviour |
|---|---|
| Start-up files | Reads ~/.mcshrc first; falls back to ~/.tcshrc then ~/.cshrc. No existing configuration needs renaming. |
| Binary | Installs as mcsh. A tcsh symlink is created alongside it so scripts that invoke /usr/local/bin/tcsh keep working. |
| Manual page | man mcsh is canonical. man tcsh is a symlink to the same page. |
| Shell variables | Both $mcsh and $tcsh are set to the running version string, so scripts guarded by if ($?tcsh) continue to fire. |
$version |
Banner reads mcsh <ver> (<origin>) … [tcsh baseline <upstream-ver>] options …, preserving the upstream tcsh version that mcsh was consolidated from. |
| Feature | Description |
|---|---|
| Interactive comments | # is a comment character in interactive mode as well as in scripts (tcsh PR #89) |
| Expression short-circuit | $?a && "$a" != "" no longer throws when a is unset; variable expansion is deferred until after the short-circuit is resolved (tcsh PR #107) |
| Pipe-to-variable | echo foo | set x and set x < file assign the piped / redirected text to x (tcsh PR #105) |
function builtin |
Named shell functions, declared as function name, then the body, terminated by return (tcsh PR #77). Declaring one also installs an alias shim that dispatches to it, so it is callable like any other command |
Redirect in { } blocks |
if ( { cmd >& /dev/null } ) correctly honours the redirection (tcsh issue #113) |
| Feature | set variable |
Description |
|---|---|---|
| Fish-style predictive autocomplete | set predict |
As you type, the most recent matching history entry, file path, or command is shown as inline ghost text (dimmed). Press Right-Arrow or ^F to accept the full suggestion. Includes a filesystem/PATH cache to ensure zero latency. |
| Interactive syntax highlighting | set syntax |
Per-keystroke ANSI colour highlighting of keywords, builtins, aliases, shell functions, commands (ok/bad), operators, variables, strings (double/single/backtick), comments, and unmatched-quote errors. Command classification follows what the shell would actually run: keywords and builtins first, then functions and aliases (which shadow $PATH), then $PATH. Arguments are classified too — options, glob patterns, and paths that exist (including ~ and ~user). Wrapper commands (sudo, env, nohup, time, xargs, …) keep the following word in command position. Also covers assignments, history references (!!, !$), variable subscripts ($argv[1]) and modifiers ($x:h). A 64-entry LRU cache avoids repeated stat(2) calls per $PATH lookup, and filesystem probes are capped per rescan. |
| Filetype colouring in completion | set color |
Coloured filetype indicators in tab-completion listings, driven by LSCOLORS / LS_COLORS. |
| Feature | Description |
|---|---|
| Git repository status | %v shows *n modified tracked files, !n unmerged paths, $n stash entries and ^ when HEAD differs from its configured upstream; %V prepends the branch and operation state. Derived by parsing .git/index and comparing it against the working tree — the same stat comparison git's own fast path makes — plus the stash log, config, and the refs (honouring packed-refs). Still no git process. Computed lazily, so a prompt using only %g never pays for it. Measured ~0.5 ms over 536 tracked files, once per poll interval. |
| Native git branch | %g expands to the current branch name (or the 7-character object name on a detached HEAD); %G also appends the operation state (main|MERGING, main|REBASING-i, main|BISECTING, abc1234|DETACHED, …). Both are empty outside a git repository. No git process is spawned — the control files are read directly. Works from any subdirectory, and in linked worktrees, submodules, and bare repos. Cached per-CWD against the resolved git directory: HEAD is compared by contents (exact — st_mtime has one-second granularity) and the state markers by mtime, tracked independently so merges, rebases and cherry-picks are detected promptly without false refreshes. Poll interval is 2s, overridable with $GIT_POLL_INTERVAL. |
| Feature | Description |
|---|---|
| Numbered tree display | pushd, popd, and cd show the directory stack as a numbered vertical list after every navigation. The current directory (index 0) is marked with →. |
dirs -v arrow marker |
dirs -v marks index 0 with → so the current position is always visible at a glance. |
cd -N |
Jumps to stack entry N counted from the bottom (oldest entry), mirroring zsh's cd -N semantics. Complements the existing cd +N (forward from current). A bare cd - still switches to $owd. |
pushd +N / popd +N |
Unchanged: rotate or pop the Nth entry from the top. |
| Token | Default colour |
|---|---|
Keyword (if, while, foreach, …) |
Bold cyan |
Builtin (set, alias, cd, …) |
Bold green |
| Alias (user-defined) | Bold blue |
| Shell function (user-defined) | Bold magenta |
Option argument (-v, --long) |
Cyan |
| Path argument that exists | Blue |
Glob pattern (*, ?, […]) |
Yellow |
Command — found on $PATH |
Green |
| Command — not found | Bold red |
Operator (|, ;, &&, …) |
Yellow |
Variable ($var, $?var) |
Magenta |
| Double-quoted string | Yellow |
| Single-quoted string | Yellow |
| Backtick substitution | Cyan |
| Comment | Bright black (grey) |
| Unmatched quote / error | Bold red |
| Fix | Description |
|---|---|
%j prompt token |
Counts only live job leaders, not all process-list entries |
getn() overflow |
@ x = (1 << 63) no longer raises "Badly formed number"; uses strtoll with overflow/errno checking |
| Shift operator UB | << and >> use unsigned arithmetic to eliminate signed-shift undefined behaviour |
crypt link failure |
AC_SEARCH_LIBS([crypt], [crypt xcrypt]) handles the modern libxcrypt split |
vms.termcap.c OOB scans |
Colon-scan loops stop at '\0'; sscanf uses `%[^ |
vms.termcap.c tgoto |
Static buffer enlarged to 64 bytes; %d uses snprintf; bounds checked throughout |
vms.termcap.c octal |
Octal digits 4–7 handled; continuation digits validated as <= '7' |
acaux/install-sh name patterns |
Case patterns use * suffix to catch multi-character names beginning with -, =, (, ), ! |
m4/lib-prefix.m4 |
dn; comment typo corrected to dnl |
m4/po.m4 C# DLL cleanup |
Error cleanup removes the actual DLL target, not the .msg source |
configure.ac patchlevel |
PACKAGE_PATCHLEVEL stripped of leading zeros, preventing invalid C integer literals like 08 |
configure.ac baseline version |
TCSH_BASELINE_VERSION correctly expands TCSH_VERSION to a quoted C string literal in config.h |
sh.func.c doif truncation |
doif() uses tcsh_number_t to avoid truncating wide expression results |
ed.defns.c catalog collision |
predict-accept uses NLS catalog ID 124 (was 122, colliding with newline-and-hold) |
ed.screen.c SGR desync |
SetSGRColor() emits ESC[22;39m (not ESC[0m) for default-fg/no-bold, preserving cur_atr synchronisation |
ed.refresh.c ghost SGR |
DrawGhost() resets with ESC[22;39m (not ESC[0m) so cur_atr stays consistent on the incremental path |
ed.inputl.c colours never drawn |
The CC_NORM path called syntax_colorize() after e_insert() had already painted the character via RefPlusOne(), and nothing redrew afterwards — so highlighting was invisible during normal typing and only appeared after an unrelated full redraw (^L, history recall, completion). The colours are now computed and repainted together, and e_insert() skips its one-character fast path while set syntax is active (that path draws raw and cannot recolour earlier characters, which a single keystroke routinely does) |
ed.screen.c colour gating |
T_CanColor came solely from the termcap Co capability, so a missing terminfo entry silently disabled highlighting outright — the common case for alacritty, kitty, foot and wezterm in containers, on servers and over ssh. Falls back to $COLORTERM, a color substring in $TERM, and a list of known colour-capable emulators. Genuinely monochrome terminals (dumb, vt100) still end up disabled |
tc.prompt.c HEAD staleness |
HEAD was compared by st_mtime, which has one-second granularity: two HEAD writes inside the same second (scripted checkouts, a TUI git client, rebase stepping through commits) left the prompt permanently stale. HEAD's contents are now compared directly — it is a ~41 byte file, so the read costs about what the stat() did and is exact |
tc.prompt.c marker mtime |
Git cache tracks HEAD mtime and state-marker max-mtime independently — a live MERGE_HEAD no longer forces a refresh on every prompt |
tc.prompt.c git cache scope |
Staleness polling watched $cwd/.git/…, a path that exists only at the root of a non-worktree checkout. The cache was therefore permanently stale in every subdirectory and in every linked worktree — %g froze on the branch that was current when you entered the directory. It now watches the resolved git directory reported by git_get_info() |
tc.prompt.c marker coverage |
The staleness watch list omitted REVERT_HEAD and BISECT_LOG, so entering or leaving a revert or bisect was never noticed. The watch list now covers every state %G can report |
tc.prompt.c double fclose |
The linked-worktree path closed the .git file, then fell through to a second fclose() on the same handle when the resolved gitdir exceeded MAXPATHLEN |
tc.prompt.c detached HEAD |
Printed the full 40-character object name: xsnprintf() parses a . immediately after % as a zero-pad flag, so "%.7s" silently meant "%07s". Truncation is now explicit |
tc.prompt.c poll throttle |
GIT_POLL_INTERVAL was defined but never used (the literal 2 was still hard-coded), the environment override was parsed with unchecked atoi(), and the throttle window was not started on a refresh so the first poll always fired regardless of the configured interval |
sh.h CHAR_EOF type |
Defined as a plain (-2) and compared against eChar, which is unsigned wint_t in the wide-character build; now cast to eChar |
dch-template.in distribution |
Template uses UNRELEASED instead of unstable |
alacritty.toml portability |
Shell invoked by name via PATH; pywal import commented out as optional |
| Escape | Expands to |
|---|---|
%g |
Current git branch name, or the 7-char object name on a detached HEAD (empty outside a git repo) |
%G |
Branch name plus operation state: main|MERGING, main|REBASING-i, abc1234|DETACHED, etc. (empty outside a git repo) |
%v |
Repository status indicators only: *n modified tracked files, !n unmerged paths, $n stash entries, ^ HEAD differs from its configured upstream |
%V |
%G plus %v — the full at-a-glance summary, e.g. main *3 $1 ^ |
%? |
Exit status of the last command |
%B / %b |
Bold on / off |
%U / %u |
Underline on / off |
%S / %s |
Standout (reverse video) on / off |
%{…%} |
Literal (zero-width) escape sequences |
%n |
Username |
%m |
Hostname (first component) |
%c02 / %~ |
Trailing 2 components of CWD / CWD with ~ substitution |
%j |
Number of running jobs |
%# |
# for root, % otherwise |
Example — right-prompt showing git branch in standout:
set rprompt = '%S%G%s'Example — full colour prompt with git and exit status:
set red = "%{\033[1;31m%}"
set green = "%{\033[1;32m%}"
set blue = "%{\033[1;34m%}"
set reset = "%{\033[0m%}"
set prompt = "${green}%n@%m${reset}:${blue}%B%c02%b${reset} [${red}%?${reset}] %# "mcsh adds zsh-style directory stack tree display and cd -N navigation.
% pushd ~/projects/foo # push new directory
0→ ~/projects/foo
1 ~/projects
2 ~
% pushd ~/etc
0→ ~/etc
1 ~/projects/foo
2 ~/projects
3 ~
% cd -2 # jump to entry 2 from bottom (oldest visible non-cwd)
0→ ~/projects/foo
1 ~/etc
2 ~/projects
3 ~
% popd
0→ ~/etc
1 ~/projects
2 ~
% dirs -v # explicit numbered listing
0→ ~/etc
1 ~/projects
2 ~
Keybindings / aliases set by dot.mcshrc:
| Alias | Command |
|---|---|
pd |
pushd |
po |
popd |
d |
dirs -v |
.. |
cd .. |
... |
cd ../.. |
The tree preserves the traditional tcsh flat layout:
| Prefix / file | Purpose |
|---|---|
sh.*.c / sh.*.h |
Core shell (parser, executor, history, jobs, directory stack, …) |
ed.*.c / ed.*.h |
Command-line editor (readline equivalent, syntax highlighting, ghost text) |
tc.*.c / tc.*.h |
tcsh extensions (prompts, key bindings, NLS, completion, …) |
tw.*.c / tw.*.h |
Tab / word completion and filetype colouring |
glob.c / glob.h |
Pattern globbing |
dotlock.c / dotlock.h |
History file locking |
mi.*, ma.setp.c |
POSIX / BSD compatibility shims |
gethost.c, host.defs |
Host-table generator |
ed.syntax.c / ed.syntax.h |
Native interactive syntax highlighting engine |
nls/ |
National Language Support catalogues |
system/ |
Per-platform compile-time config fragments |
acaux/, m4/ |
Autoconf / autotools auxiliary files |
configure.ac, Makefile.in |
GNU Autotools build system |
complete.mcsh, complete.tcsh |
Programmable completion rules |
csh-mode.el |
Emacs major mode for csh/mcsh scripts |
tcsh.man.in |
Manual page template |
dot.login, dot.tcshrc, dot.mcshrc |
Example user start-up files |
make # build in-tree; produces ./mcsh
sudo make install # installs mcsh + tcsh symlinkautoreconf -fi # regenerate configure (needed once after editing configure.ac)
./configure
make
sudo make installmcsh has no native Win32 support. Build inside WSL:
sudo apt install build-essential autoconf automake
autoreconf -fi && ./configure && makeautoreconf -fi && ./configure && makedot.mcshrc is the canonical start-up file. Copy it to ~/.mcshrc:
cp dot.mcshrc ~/.mcshrcSections and what they provide:
| Section | Key settings |
|---|---|
| 1 — Display server | Wayland env vars gated behind /dev/dri/card0 + $WAYLAND_DISPLAY presence check. Machine-specific GPU overrides go in ~/.mcshrc.local. |
| 2 — System environment | Prepends ~/.local/bin to $path; sets EDITOR, VISUAL, PAGER, LESS, BLOCKSIZE, CLICOLOR, LSCOLORS. |
| 3 — Core execution engine | set autorehash, autolist=ambiguous, autoexpand, autocorrect, color, syntax, correct=cmd, ellipsis, filec, listjobs=long, listlinks, listmax=100, matchbeep=never, rmstar, symlinks=chase; history 10 000 entries with merge-dedup to ~/.mcsh_history. |
| 4 — Key bindings | Emacs mode; Up/Down arrow history-search; Ctrl+Arrow word navigation; Home/End for xterm/vt100/rxvt/application-cursor; magic-space, backward-delete-word, run-fg-editor, kill-region. |
| 5 — Completions | cc/clang/gcc (file extensions + -I/-L); make (reads live target list); man, kill, sysctl, service, ifconfig, cd, tar/gzip/xz/bzip2. |
| 6 — Aliases | ls -F -G, l, ll, df -h, du -ch, .., ..., pd/po/d (pushd/popd/dirs), dis (objdump Intel syntax), cclean, h, j, m, g. |
| 7 — Prompt | %g/%G git escapes; colour-coded prompt with user@host, CWD, exit status; rprompt='%S%G%s'; prompt2 and prompt3 for multi-line and correction. |
| 8 — Host completion | Builds $hosts from ~/.hosts, ~/.rhosts, ~/.ssh/known_hosts for SSH/rlogin completion. |
| 9 — System-specific | Sets stty status ^G + binds stuff-char on BSD/Darwin/FreeBSD/NetBSD; set time coloured format at the end so startup commands are not timed. |
| Root guard | Unsets savehist; sets LESSHISTFILE=- and VIMINIT='set viminfo=' when $uid == 0. |
| Local overrides | Sources ~/.mcshrc.local last if it exists — machine-specific GPU vars, paths, tokens go there. |
- mcsh sources
~/.mcshrcon startup, falling back to~/.tcshrcthen~/.cshrc. No existing configuration needs to be renamed. complete.mcshis the mcsh-native completion file.complete.tcshis retained for legacy setups that test$?tcsh.- The
tcshbinary symlink created bymake installensures existing scripts and/etc/shellsentries keep working.
Multi-byte characters — emoji, CJK, Latin Extended, and any character whose UTF-8 encoding is longer than one byte — were previously silently dropped or corrupted during filename glob expansion and variable assignment. This was a byte-vs-character length bug inherited from tcsh 6.24.14 (tracked as tcsh issues #117 / #121).
Resolution (sh.lex.c, sh.dol.c): the mbtowc accumulation loops in
wide_read() and the $< line-read primitive now bound partial-byte
lookahead by the runtime MB_CUR_MAX instead of the compile-time
MB_LEN_MAX. After a stray invalid byte the loop no longer over-reads up to
15 bytes of subsequent valid UTF-8.
Tests: tests/t009_unicode_vars.sh through
tests/t014_unicode_script_source.sh cover variable round-trip,
$% character count, glob expansion, $< stdin read, backquote
substitution, invalid-byte recovery, and sourced-script Unicode.
See ISSUES.md Round 9 for details.
if ($?a && "$a" != "") previously threw "Undefined variable" even when
$a was unset because Dfix() expanded all $ tokens before &&
short-circuiting could suppress evaluation. Fixed in sh.dol.c: unset
variables now silently expand to "", matching bash/zsh semantics.
Modifier expressions like ${unset:h} and dimen expressions like $#unset
also no longer error — they return "" and 0 respectively.
See ISSUES.md Rounds 6–7.
An initial regression suite is in tests/ covering startup variables,
arithmetic overflow/shift semantics, short-circuit evaluation, pipe-to-var,
directory stack, the function builtin, signed right-shift, and unset variable
modifier handling. This covers the core new features but is not exhaustive. Run with:
tests/run_tests.shmcsh is BSD 3-Clause (see LICENSE). The upstream tcsh / etcsh source is also BSD 3-Clause (see UPSTREAM-COPYRIGHT). Redistribution must carry both notices — see NOTICE for details.
mcsh is built on the shoulders of two excellent upstream projects:
| Project | Repository | Role in mcsh |
|---|---|---|
| tcsh | github.com/tcsh-org/tcsh | The authoritative upstream shell engine. Originally written by Ken Greer and Mike Ellis at Carnegie Mellon, extended by Paul Placeway, and now maintained by Christos Zoulas and the tcsh community. The bulk of sh.*, ed.*, tc.*, tw.*, NLS, and build system derive from this project. |
| etcsh | github.com/Krush206/etcsh | An enhanced fork of tcsh by Krush206, adding function declarations, interactive comments, pipe-to-variable, expression short-circuit fixes, and other language improvements — all of which are incorporated into mcsh. |
All original copyright notices are preserved in UPSTREAM-COPYRIGHT and NOTICE.
Bug fixes and feature PRs that originated in the upstream repositories are
credited in ISSUES.md and PLAN.md with their upstream issue/PR numbers.
See ISSUES.md for the running log of bugs, compatibility items, and modernisation tasks.
See PLAN.md for the full phased execution plan.