novaShell is a small command shell for Windows, written in C as a single source file. It only uses the Win32 API and the C runtime, so there are no dependencies and nothing to install. The goal is to bring a few Linux-style conveniences to Windows (history search, bookmarks, background jobs, grep and friends, saved SSH connections) while still being able to run everything that works in cmd.exe.
Anything that is not a built-in command is handed to cmd.exe, so batch files, pipes, redirects and normal programs behave as usual.
- Building
- Starting out
- The prompt
- Line editing
- Tab completion
- Command reference
- Chaining, pipes and background jobs
- Exit status and notifications
- SSH profiles
- Configuration
- Limits and known quirks
The whole shell lives in main.c.
With MSVC (from a developer command prompt):
cl /O2 main.c
With MinGW-w64:
gcc -O2 main.c -o novashell.exe
A 64-bit build is recommended. The OpenSSH client that ships with Windows lives in System32\OpenSSH, and a 32-bit process on 64-bit Windows gets redirected away from that folder, so sshm would not find ssh.exe.
- The "OpenSSH Client" optional feature (Settings, Apps, Optional features) is needed for everything under
sshm.
Run novashell.exe. Type help for a short summary of everything below.
A few things worth knowing right away:
- Aliases are expanded before built-ins, so an alias can point to a built-in (the default
..alias iscd ..). cdis a real built-in, so the directory change sticks. The same goes forexportandset NAME=value.- Lines starting with
#are ignored. That is mostly useful in script files (because why would you use that in the terminal?). - If input is piped into novaShell instead of typed, it reads plain lines without the line editor.
The default prompt looks like this:
[1] ~\projects\site (main)>
[1]is the exit code of the previous command. It is only shown when the code is not zero, in red. Large codes are shown in hex.- The home folder is abbreviated to
~. (main)is the current git branch, shown in a different color inside a git repository. For a detached HEAD, the short commit hash is shown.
The prompt command sets the format and saves it to prompt.txt:
prompt "{user}@{host} {cwd}{git}{jobs}> "
prompt reset
prompt
Quotes are optional, but they are the way to keep a trailing space. Running prompt without arguments shows the current format.
| Token | Meaning |
|---|---|
{code} |
Red badge with the last exit code (empty when it was 0) |
{cwd} |
Current directory, with ~ for the home folder |
{git} |
Current git branch in parentheses (empty outside a repository) |
{time} |
Current time as HH:MM:SS |
{user} |
User name |
{host} |
Computer name |
{jobs} |
Number of running background jobs, like [2 bg] |
{nl} |
Line break, for a two-line prompt |
Unknown tokens are printed as they are. Setting the environment variable NOVA_NOGIT=1 turns off the git lookup, which can help on slow network drives.
novaShell has its own line editor instead of the plain console input.
| Key | Action |
|---|---|
| Left / Right | Move the cursor |
| Home / End | Jump to start or end of the line |
| Ctrl+A / Ctrl+E | Same as Home / End |
| Ctrl+Left / Right | Jump by word |
| Backspace / Delete | Delete before or under the cursor |
| Ctrl+W | Delete the previous word |
| Ctrl+U or Esc | Clear the whole line |
| Up / Down | Walk through history |
| Ctrl+R | Incremental history search (see below) |
| Tab | Complete; press again to cycle through candidates |
| Right or End or Ctrl+E at the end of the line | Accept the ghost suggestion |
| Ctrl+D on an empty line | Exit the shell |
While typing, the most recent history entry that starts with the current text is shown dimmed after the cursor. Pressing Right, End or Ctrl+E at the end of the line accepts it. The suggestion is hidden while searching history and when Enter is pressed.
- History is stored in
history.txtand keeps the last 500 entries. Consecutive duplicates are not stored twice. history [n]prints the whole history or the lastnentries.!!runs the previous command again,!Nruns entry numberNfrom thehistorylisting. The expanded line is echoed before it runs.- A line that starts with a space is not saved to history. That could be handy for commands with secrets in them.
history-clearwipes the history.
Press Ctrl+R and start typing. The newest entry containing the typed text (case-insensitive) is shown. Ctrl+R again jumps to the next older match. Enter or an arrow key puts the match on the line without running it, and Esc cancels.
Completion depends on where in the line the cursor is.
- First word: built-in commands, aliases and executables found on
PATH(.exe,.bat,.cmd). - Paths: files and folders, with
~and%VAR%expanded. Hidden dot-files only show up when the typed fragment starts with a dot. Aftercdonly directories are offered. - Environment variables: after an unclosed
%, variable names are completed (echo %PATH%). Afterexport,setandunset, variable names are offered too. - git: subcommands after
git, and branches, remote branches and tags after subcommands likecheckout,switch,merge,rebase,branch,diff,log,reset,showandcherry-pick. Names with slashes such asfeature/loginwork. Files are still offered next to the refs. - ssh, sftp, scp: host names from
~\.ssh\known_hosts(unhashed entries only) andHostaliases from~\.ssh\config. Wildcard hosts are skipped.user@ho<Tab>keeps the user part. Forscp, local files are offered as well. - sshm and bm: subcommands and profile or bookmark names. See the sections below.
- alias, unalias: alias names.
When several candidates match, the common prefix is filled in first and the candidates are listed in columns. Pressing Tab again cycles through them.
| Command | What it does |
|---|---|
cd [path] |
Change directory. Without arguments it prints the current directory. |
cd - |
Go back to the previous directory. |
cd ~\Documents |
~ stands for the home folder. |
pwd |
Print the current directory. |
mkcd <path> |
Create a directory (including missing parents) and enter it. |
dir [path or pattern] |
Colored directory listing. |
cd also understands the cmd.exe habits cd /d X, cd.. and cd\.
dir accepts a folder, a single file or a wildcard such as dir *.txt. Folders are cyan, executables green and archives red. If the arguments start with a switch (dir /b, dir /s) or the line contains a pipe or redirect, the real cmd.exe dir is used instead.
Bookmarks are named directories, stored in bookmarks.txt.
bm add proj C:\code\project save a bookmark (without a path it uses the current directory)
bm proj jump to it
bm list bookmarks
bm rm proj delete a bookmark
Bookmark names may contain letters, digits, ., _ and -.
| Command | What it does |
|---|---|
export NAME=value |
Set an environment variable. %VAR% in the value is expanded. |
set NAME=value |
Same as export. Plain set still lists variables through cmd.exe. |
unset NAME |
Remove a variable. |
env |
List all variables. |
Variables set this way are visible to every program started afterwards, and they last until the shell exits. To make them permanent, put the commands into startup.txt.
alias list all aliases
alias ll show one alias
alias ll dir /b define (or update) an alias
alias ll="dir /b" same, with = syntax
unalias ll remove it
Aliases are saved to aliases.txt right away. On the very first run a few defaults are created (ls, list, ll, clr, .., ..., cp, mv, rm). After that the file is the only source of truth, so a removed default stays removed.
An alias only replaces the first word of a line, and the rest of the line is appended. An alias that refers to itself (ls=ls -la) is expanded once and not repeatedly.
If the alias text contains $1 to $9 or $*, the typed arguments are inserted at those places and nothing is appended automatically.
| Placeholder | Meaning |
|---|---|
$1..$9 |
The first to ninth argument |
$* |
All arguments, as typed |
$$ |
A literal dollar sign (but why would you?) |
alias mkproj=mkcd ~\code\$1 && git init
mkproj webapp
Missing arguments become empty text. Arguments containing spaces should be quoted when typing.
| Command | What it does |
|---|---|
source <file> |
Run the commands in a text file, one per line. Empty lines and # lines are skipped. |
time <command> |
Run a command and print how long it took. |
touch <files> |
Create files, or update the timestamp of existing ones. |
startup.txt in the configuration folder is run like a source script every time novaShell starts. It is a good place for export lines, alias lines or a cd.
When cd (or bm, or mkcd) enters a folder that contains a file named .novarc, novaShell can run it. Because that is code running just from entering a folder, it asks first:
Run this .novarc? [y] once [a] always (this version) [N] no:
yruns it this time only.atrusts this exact content. A hash of the file is stored intrusted_rc.txt, so as soon as the file changes, novaShell asks again.- Anything else skips it.
The same applies to the folder novaShell is started in. Setting NOVA_NORC=1 disables the feature. A .novarc is a normal script, so export, alias and source venv\Scripts\activate.bat-style lines work. Variables it sets are not undone when leaving the folder.
| Command | What it does |
|---|---|
sudo <command> |
Run a command elevated. A UAC prompt appears, the command runs in a new window that waits for a key press at the end, and novaShell continues when it is closed. |
sudo |
Open a new elevated novaShell. |
which <name> |
Say whether a name is an alias, a built-in or an executable (with its path). |
title <text> |
Set the console window title. |
clear, cls |
Clear the screen. |
help |
Print the built-in summary. |
about |
Version information. |
exit, quit |
Leave the shell. |
The config command helps with the configuration files.
config show the folder and the file names
config open open the folder in Explorer
config edit edit startup.txt in Notepad
config edit aliases also: ssh, bookmarks
config reload re-read aliases, ssh profiles and bookmarks after editing by hand
cat, head, tail, wc and grep are built in, so they exist on any machine. They read the files given as arguments (wildcards like *.log work), or standard input if no file is given.
| Command | Details |
|---|---|
cat [-n] [files] |
Print files. -n numbers the lines. |
head [-n N] [files] |
First 10 lines, or N lines. -5 is short for -n 5. |
tail [-n N] [-f] [files] |
Last 10 lines, or N. -f keeps following a single file until Ctrl+C. |
wc [-l] [-w] [-c] [files] |
Count lines, words and bytes. Without flags all three are shown. |
grep [options] pattern [files] |
Search for text, see below. |
With more than one file, head and tail print a ==> name <== header per file. -q hides the headers.
grep does a plain substring search. Regular expressions are not supported.
| Option | Meaning |
|---|---|
-i |
Ignore case |
-v |
Show lines that do not match |
-n |
Show line numbers |
-c |
Only print the number of matching lines per file |
-l |
Only print the names of files that contain a match |
-r |
Search folders recursively. Without a file argument the current folder is searched. |
-H |
Always print the file name |
-e |
Take the next argument as the pattern (for patterns starting with -) |
Options can be combined, like -rin. File names are shown automatically when more than one file is searched. Matches are highlighted when output goes to the console. The exit code is 1 when nothing matched, which shows up in the prompt badge and works with &&.
grep -rin "todo" *.c
dir | grep txt
type log.txt | grep -i error | wc -l
a && b runs b only if a succeeded. novaShell handles this itself, so built-ins can be part of a chain (cd build && make). Quoted && is left alone. A line containing parentheses is passed to cmd.exe untouched, so constructs like if exist x (a && b) still work.
Pipes and redirects are done by cmd.exe. The text tools are built into novaShell, which cmd.exe knows nothing about. To make them work inside a pipeline, novaShell rewrites the first word of each pipeline segment: dir | grep txt is run as dir | "C:\path\novashell.exe" --nova-exec grep txt. A small child instance of novaShell then runs the tool with the pipe as input and output. This is invisible in normal use.
A line ending in a single & runs in the background:
ping -t example.com &
sshm tunnel web 8080:localhost:80 &
Built-ins can be backgrounded too, because a child novaShell is used for them.
| Command | What it does |
|---|---|
jobs |
List jobs with their number, state and process id. |
fg [n] |
Wait for a job (the newest one without an argument). Ctrl+C stops waiting and leaves the job running. |
kill %n |
Stop job n including everything it started. |
kill <pid> |
Stop any process by id. |
Details:
- On start, the shell prints
[1] 12345(job number and process id). When a job finishes, a[1]+ Done (exit 0)line appears before the next prompt. - Background jobs get no keyboard input and ignore Ctrl+C typed at the prompt.
- Their output goes to the same console, so it can show up in the middle of typing.
- Jobs are tied to novaShell. When the shell exits, they are stopped.
After every external command (anything handed to cmd.exe) a status line is printed:
| Output | Color |
|---|---|
Process exited with code 0 (Success) |
green |
Process exited with code 1 (General Error) |
red |
Process exited with code 0xC000013A (Process terminated by CTRL-C) |
red |
Process exited with code 5 (any other code, no meaning) |
yellow |
Built-ins do not print this line. It is meant to be extended: new meanings can be added in the print_exit_status function in the source.
If an external command takes 30 seconds or longer, novaShell prints how long it took (for example Took 1m 23s) and rings the terminal bell. Windows Terminal turns the bell into a taskbar flash. The threshold can be changed with the environment variable NOVA_NOTIFY_SECS. Setting it to 0 turns the feature off.
sshm stores connections that are used often, so a long ssh line turns into a short name. It uses the OpenSSH client (ssh.exe and scp.exe) that comes with Windows. Profiles are stored in ssh_hosts.txt.
sshm add web admin@example.com -p 2222 -i ~\.ssh\id_ed25519
sshm list
sshm web
sshm web uptime
sshm add <name> [user@]host takes these options:
-p <port>sets the port.-i <keyfile>sets the identity file (~is expanded).- Everything else is stored as extra ssh options and added on every connection, for example
-L 8080:localhost:80or-o ServerAliveInterval=30.
Adding a name that already exists updates it. sshm <name> (or sshm connect <name>) connects, and extra words after the name are run as a remote command.
| Command | What it does |
|---|---|
sshm list |
Show all profiles (also what plain sshm does) |
sshm add <name> [user@]host [options] |
Save or update a profile |
sshm connect <name> [command...] |
Connect, optionally running a command |
sshm last [command...] |
Connect to the profile that was used most recently |
sshm show <name> |
Show the profile and the exact command line that will run |
sshm rename <old> <new> |
Rename a profile |
sshm rm <name> |
Delete a profile |
sshm test <name> |
Check that the host is reachable and key login works |
sshm tunnel <name> [-L|-R|-D] <spec> |
Open a port forward without a shell |
sshm copy-id <name> [pubkey] |
Install a public key on the server |
sshm import [file] |
Import profiles from an OpenSSH config file |
sshm put <name> [-r] <local> <remote> |
Upload with scp |
sshm get <name> [-r] <remote> <local> |
Download with scp |
- test runs
sshin batch mode with a 5 second timeout and does not ask for passwords. A host that only accepts passwords therefore reports a failure, even though it is reachable. - tunnel defaults to a local forward (
-L). The spec is the usual ssh syntax, like8080:localhost:80.-D 1080opens a SOCKS proxy. The tunnel stays open until Ctrl+C. Putting&at the end runs it as a background job. - copy-id pipes the public key into a small script on the server that appends it to
~/.ssh/authorized_keysand skips it if it is already there. The password is asked for once. It is meant for Linux and Unix servers. Without an explicit file, it uses the profile's key with.pubadded, thenid_ed25519.pub, thenid_rsa.pubfrom~\.ssh. - import reads
~\.ssh\configby default. It picks upHostName,User,PortandIdentityFilefromHostblocks. Wildcard patterns andMatchblocks are skipped, and profiles that already exist are never overwritten. - put and get pass the port and key of the profile to scp. Flags starting with
-(like-r) go straight to scp.
Profile names, user names and host names are checked before use (letters, digits and a few separators, never starting with -), and ssh is started directly instead of through cmd.exe. This is intended because it keeps a mistyped or hand-edited profile from turning into an option or a shell command. Lines in ssh_hosts.txt that fail these checks are ignored, and a warning with the count is printed at startup.
Everything lives in %APPDATA%\novaShell. config shows the exact path.
| File | Purpose |
|---|---|
aliases.txt |
One name=command per line |
startup.txt |
Commands run at every start |
history.txt |
Command history, one entry per line |
ssh_hosts.txt |
SSH profiles, tab-separated fields |
ssh_last.txt |
Name of the most recently used SSH profile |
bookmarks.txt |
Directory bookmarks, tab-separated |
prompt.txt |
Saved prompt format |
trusted_rc.txt |
Hashes of .novarc files that were trusted |
All files are plain text and can be edited by hand. After editing aliases, ssh profiles or bookmarks while the shell is running, config reload picks up the changes.
| Variable | Effect |
|---|---|
NOVA_NOGIT |
Set to anything to skip the git branch lookup |
NOVA_NORC |
Set to anything to ignore .novarc files |
NOVA_NOTIFY_SECS |
Seconds a command must run before the bell and "Took ..." line (default 30, 0 turns it off) |
- The line is limited to 4096 characters. There is room for 500 history entries, 64 aliases, 128 SSH profiles, 64 bookmarks and 32 background jobs.
grepmatches plain text, not regular expressions.- The text tools read files in text mode, so
wc -ccounts after line ending conversion and can differ from the real file size on files with Windows line endings. - The line editor counts one character as one console column. Wide characters (some CJK text, emoji) can throw the cursor position off.
- Ctrl+C at the prompt does nothing. Ctrl+C while a command runs goes to that command.
- Background job output is written straight to the console, so it can interleave with the prompt.
cdwithout arguments prints the current directory (likecmd.exe) instead of jumping home.cd ~does the latter.- Only a trailing single
&starts a background job.a & bin the middle of a line is handled bycmd.exe. .novarcchanges to the environment are not reverted when leaving the folder.sshm copy-idassumes a Unix-like server. Windows OpenSSH servers keep authorized keys in a different place.
Annabeth Kisling