Package: demosh
demoshtl;dr:demoshcan run shell scripts or Markdown files in a very
interactive way, showing commentary in between running commands, and waiting
for you to hit RETURN to proceed before running commands. It was created as
a tool for doing live demos of relatively complex things. Seetesting.mdandtesting.shfor examples.For instructions on installingdemosh, seeINSTALLING.md.If you want to work ondemoshitself, seeDEVELOPING.md.For a demo ofdemoshitself, seedemo/DEMO.md.demoshis a demo shell: it reads shell scripts or Markdown files and
executes shell commands from them. However, it can also output commentary
from the script, show commands before running them, and pause before (or
after) running each command. Pausing and what to show can be controlled by
inline comments in the script itself. Seetesting.mdandtesting.shfor
examples.RunningRundemosh path-to-script [arg [arg [...]]]as ifdemoshwere itself the
shell.demoshstarts outnotshowing commentary andnotbeing interactive,
so that the script can do initialization quietly. Use the@SHOWdirective
to switch into the fully-interactive mode (and use@HIDEto go back).Init Scripts and BuiltinsOn startup,demoshwill load$HOME/.demoshrcand$HOME/.demoshrc.mdif
they exist, parsing.demoshrcas a shell file and.demoshrc.mdas a
Markdown file. The--no-initflag prevents this behavior.These files are treated exactly the same as files supplied on the command
line. In particular,@SHOWdirectives will cause them to go interactive (as
discussed below), and definitions will be available for files on the command
line to use. There's no need to supply either, and no reason to supply both.demoshwill also load a set of builtin definitions on startup, unless the--no-builtinsflag is present on the command line. Builtins are discussed
below with other directives.Executing and WaitingWhendemoshhas a command to execute in interactive mode, it will:Optionally type the command slowlyOptionally wait for a RETURNRun the commandOptionally wait for a RETURNBy default, typing the command and waiting for RETURNbeforeexecution are
enabled, and waiting for RETURN after executing the command is disabled.While "waiting for RETURN", there are several things you can actually type:Hittingspacewhile the command is being typed will type the rest of
the command immediately.HittingRETURNwhile the command is being typed will type the rest of
the command immediately and then execute it immediately.HittingQwill quitdemosh. This must be a capitalQ; lowercaseqwill do nothing.Hitting-will repeat the previous command (note that this currently
doesn't work well when executing a macro).Hitting+will skip to the next commandwithoutexecuting this one
(note that this currently doesn't work well when executing a macro).Hitting!will spawn a subshell with all the environment variables
defined in the script intact.NOTE WELL: the subshell will not, at present, include functions
defined in the script.Whendemoshhas a command to execute in noninteractive mode, it just
executes it.DirectivesWhen reading from shell scripts,demoshdirectives look like comments:#@SHOW#@HIDEetc.There must not be any spaces between#and@-- only#@can
start a directive.When reading Markdown, directives can still be in comments inbashblocks,
but most of the time it works better to put them in Markdown comments:<!--@SHOW-->
<!--@HIDE-->etc.Note that there is no leading#in this case.Valid directives:@SHOW: start showing commentary, displaying commands before running them,
waiting for the user to hit RETURN before running things, etc. This isdemosh's fully-interactive-during-a-demo mode.@HIDE: the inverse of@SHOW. Just run commands without showing comments
or waiting.demoshstarts in this mode.@SKIP: don't doanythinguntil an@SHOWdirective -- don't show
commentary and don't run commands. This is mostly a debugging aid: when
working out what a demo should contain, it's very handy to have an easy
way to skip sections entirely and focus in on the parts that aren't
working so smoothly.@wait: wait for RETURN before proceeding. This can be useful for e.g.
pausing while showing longer blocks of commentary.@waitafter: waitafterrunning the next command, as well as before.@nowaitbefore: donotwait before running the next command.@noshow: do not display the next command.@notypeout: don't slowly type the next command, just spit it out all at
once.@immedor@immediate: don't display the next command and don't wait
before or after it. This is a way to run a command inline without showing
it to the viewers.@print arg [arg [...]]: like the shell'sechocommand, but it colorizes
the output as appropriate. For example:#@print # This will be colorized like a commentwill output text that's red like an inline comment. Also, since@printhas to be a directive, it will never produce output when running the
script using the normal shell.@import: see "Imports" below.@macro: see "Macros" below.@hook: see "Hooks" below.@ifhook: see "Hooks" below.Some "directives" are actually macros defined as builtins:@wait_clearwaits for RETURN, then clears the screen.@browser_then_terminaltries to use hooks namedshow_browserandshow_terminalto wait, show a browser, then return to the terminal. Both
must be defined for the macro to have any effect.@start_livecast, likewise, tries to use hooks namedshow_slidesandshow_terminalto show a slide deck, wait, clear the terminal screen, and
show the terminal. Again, both hooks must be defined for the macro to have
any effect.Finally, any other directive will be interpreted as an immediate command, so
(in shell mode):#@foobaris exactly like#@immedfoobarmeaning that it will immediately runfoobaras a shell command: no command
display, no waiting. The main difference is that the one-line version looks
like a comment if you're executing the scriptwithoutdemosh, so if
you're running your script withbashit willnotrunfoobarat all. The
two-line version leaves thefoobarcommand looking like a command even when
running withoutdemosh.ImportsThe@import <pathname>directive reads the given pathname and inserts its
contents into the input stream. This is mostly a way of getting annoying
setup code out of the main script, to make the main script easier for others
to read.You can import Markdown into shell scripts and vice-versa:@importwill
read its argument as Markdown if the pathname ends in.md, or as shell
otherwise.MacrosMacros allow giving groups of commands simpler names. They areverysimple: running a macro is the moral equivalent of just inserting the macro's
contents into the script in place of the macro. They don't take arguments and
they don't have any dynamic behavior.The syntax is as follows:#@macro macro-name#@command1#@command2...#@endThen, later:#@macro-namewill runcommand1,command2, etc.It's not technically necessary to have every command in a macro start with#@; likewise, it's not strictly necessary to invoke the macro with a
leading#@. It is usually a good idea, though, since it means that all
macros will be no-ops when running the script withoutdemosh.HooksHooks are a special kind of macro-ish thing that create functions based on
environment variables whose names start withDEMO_HOOK_.#@hook do_the_thing THINGwill causedo_the_thingto execute the contents ofDEMO_HOOK_THINGif
that variable is present in the environment, or to be a no-op if it's not
set. This provides a simple mechanism to e.g. control a livestreaming setup
by setting environment variables, and still have a working script if they're
not set.Note that hooks can appear in macros, and that, again, it's a good idea to
only invoke hooks with the#@prefix so that the calls are ignored if you
run the script withoutdemosh.There is also a special form,@ifhook, for hooks:#@ifhook hookname#@command1#@command2...#@endifin which the commands enclosed between@ifhookand@endifwill be run only
if the named hook is not a no-op. This permits, for example, skipping a
sequence with multiple@waits if it's not going to have any useful effect,
so that the user doesn't have to press RETURN multiple times to continue.Limitationsdemoshhas some serious limitations, mostly stemming from two things:Shell syntax isterrifyinglycomplex.demoshdoesn't even try to be a
full shell parser.demoshexecutes commands one at a time, rather than messing about with
trying to have a persistent shell process.Parsingdemoshdoesn't even try to fully parse the insanity of shell syntax.
Instead, it does things more simply:When reading shell scriptsLines starting with#are considered commentary, and will be displayed in
interactive mode. Blank lines are considered commentary, too, but multiple
blanks are folded into one.Oncedemoshsees a line that doesn't look like a comment, it reads lines
until an unescaped newline that's not inside curly braces is found. This
forms a single command."Unescaped" means that the newline is not preceded by a backslash. The
curly-brace thing is for shell functions. We doNOTparse quoted
strings at present; I don't see the benefit for demo scripts.When reading MarkdownAnything outside a code block is considered commentary. Multiple blank
lines will be folded into one.Within a code block marked as eitherbashorsh, lines get parsed as ifdemowere reading a shell script.ProcessingSincedemoshexecutes commands in independent subshells, it has to do some
complex stuff to make things appear to be a single shell session.demoshkeeps track of environment variables on its own. Immediately
after reading a command, anything of the form${VARNAME}is
interpolated with the value fromdemosh's environment.NOTE WELL:onlythe curly-brace form is interpolated. The unbraced
form$FOOand complex forms like${FOO:-default}or the like arenothandled, because that way lies madness without alotmore work in the
parser.Also note that positional variables ($1etc) will be taken fromdemosh's command line itself: see below for more.If the command looks like an environment variable assignment, we updatedemosh's environment internally. This is the environement passed to each
command being executed."Looks like an assignment" means that it has optional whitespace, then
an identifier followed immediately by an equals sign, then optional
content.If the command looks like a shell function definition, we save the line
and prepend it to all subsequent shell commands. Again, separate
subshells.demoshdoesnotprepend functions to thecdcommand or
variable assignments.Only theidentifier () {andfunction identifer () {forms of
function definitions are supported.If the first word of the command iscd, we handle that in thedemoshitself.Assignments andcdare both handled by running the command in a new shell
and then having the shell echo the result back to us, so that the shell can
manage more complex variable expansions, etc.Why do things like this? Because it's pretty simple and it generally works
for demos. An alternative would be to use a single shell and drive it using
aptyand multiple threads, but signal handling ismuchharder in that
world.Environment variablesdemoshmaintains the environment passed to each command. Whendemoshstarts, it saves its own command line in this enviroment as the positional parameters:$0is the script passed todemosh;$1etc. are command-line parameters after the script; and$SHELLisdemoshitself (as a fully-qualified path).Signal HandlingWhen executing a command, you can useINTR(usually control-C) as usual to
interrupt the command.demoshignoresSIGINTandSIGTERMso that you
can't accidentally interruptdemoshitself.License and Copyrightdemoshis copyright 2022 Buoyant, Inc., and is licensed under the Apache
License Version 2.0. For more information see theLICENSEfile.