The search bar as a shell

Pipelines

The overlay's search bar doubles as a small shell. A source, a chain of filters, and somewhere to send the result — written the way you would write it in a terminal.

Text that starts with $ or |, or that contains an unquoted |, is a pipeline rather than a search. Everything else searches as before: neither character means anything to the fuzzy finder, so taking them over cannot change what an existing search does.

A pipeline is a source followed by filters:

$1 | trim | upper
$jiraBaseURL ++ /browse
| trim                       ← a leading | starts from $1
trim | upper                 ← and it can be left off before a filter
"hello" | upper              ← or start from text written out
$1 | trim > $tidy            ← the result goes to a variable
$1 | split , >> @recipients  ← or onto the end of a list
$1 + 5                       ← an operator separates steps like a |
$1 ++ " (copied)"            ← and ++ joins text on the end
$1 ++ $($2 | strftime %F)    ← a $( … ) is a pipeline inside a pipeline

Text in quotes is a pipeline too when it does something: when a step follows it, or when there is a $ inside it. A quoted phrase on its own is still a search, since quotes round a phrase are an ordinary thing to type into a search box.

References

The clipboard history is addressable as if it were variables. $1 is the most recent item, $2 the one before it, and so on for as far as the history goes. $name is the variable of that name, and ${name} is the same thing — the braces are for a name the text around it would otherwise run into.

Because positions and names share one namespace, no variable may be named a whole number: such a name could never be reached, so the Variables pane refuses it, storing one from the overlay is refused, and the engines ignore it.

$!name is a secret — a variable kept encrypted, which reads only once you have proved you are there. The ! counts at the head of a name and nowhere else, so $not!aname is one name with a ! in the middle of it. @!name reads the same secret as a list, by its lines.

References expand inside filter arguments too, whatever they are quoted with. $$ is a literal $, and a reference to something that does not exist is left as written.

Note that a pipeline's result becomes the newest history entry, so $1 then refers to it and everything below shifts down by one.

Text written out

A pipeline can start from text in quotes instead of from something that was copied:

"hello" | upper
'Dear $name,' ++ $1
"hello@$($domain | trim)"

Either quote does it, and the quotes are the whole of the source — "a" b is two things where one was expected, and is refused. A | inside the quotes is a character rather than a separator, so a whole command can sit inside one.

What is written there is expanded exactly as a filter's argument is: $name, $1, ${name} and $( … ) all stand for what they name, and $$ is a literal $. Both quote styles expand alike, which is the rule everywhere else in a command.

Text written out is the one source that is a whole pipeline on its own — every other one is somewhere to read from. So a pipeline action of just "hello" is an action that replaces the text with hello. It is not somewhere a result can be sent: > "name" names nothing, and is refused.

eval: the same expansion, on text you stored

Quotes only expand what you type into the bar. eval does the same thing to text that was already there — it reads what it is handed as if it had been written out in quotes:

$greeting                  Dear $name,
$greeting | eval           Dear Marion,

That is the difference between storing a sentence and storing a template. A variable full of $s is just characters until this runs on it, and then every $name, $1, ${name} and $( … ) in it stands for what it names:

$line | eval               Sent $(| now | strftime %H:%M)
$1 | eval                  expand what you just copied

A name that stands for nothing is left exactly as written, the same answer an argument gets — so a $ that was only ever a dollar sign survives, and $$ is how to write one that must not expand.

It may not reach itself: text that evaluates to text that evaluates would never finish, so a second eval inside one is refused rather than run. Over a list it is map that runs it, an item at a time.

Filters

A filter is an action, named by its keyword. A keyword is a single word, so the first word of a step is always the filter and everything after it is arguments — join-lines , is the filter join-lines with the argument ,.

Quoting keeps an argument as one token, and decides nothing else. Single and double quotes are interchangeable, and references expand inside both: 'hello ${who}' and "hello ${who}" are the same argument. This is deliberately not the shell's rule. A quote may sit in the middle of a token, so a"b c"d is the one argument ab cd, and "" is an empty one. Which leaves $$ as the way to write a literal $ — '$5.00' is the fifth clipboard item followed by .00, and '$$5.00' is the price.

The first argument a filter receives is always the text from the previous stage, or the value of the source for the first filter. For a script that is $1, exactly as when a trigger runs it, and the pipeline's extra arguments follow as $2 onwards.

An unknown filter, or one that fails, stops the pipeline and is reported — except for a name still being typed at the very end of the bar, which is not an error yet. That one is previewed as far as the filters before it, and the rows below offer the names it could become.

Arithmetic

The four operators +, -, * and / separate steps in place of a |, so that a sum reads as one:

$1 + 5
$1 * 1.2 - 0.5
$subtotal + $shipping
$1 | trim + 5      ← mixed freely with ordinary filters

That is the whole of the syntax. Each operator is an ordinary filter whose keyword is the operator itself and whose argument is the number, so $1 + 5 and $1 | + 5 are the same command written two ways.

Which means there is no operator precedence. Steps run in the order they are written, arithmetic included, because each sum is a whole step rather than part of an expression: $1 + 2 * 3 multiplies the sum by three and does not add six. This is the one place the search bar reads differently from the thing it looks like, so a command mixing + or - with * or / warns, spelling out both readings.

An operator needs a space either side of it. That is what keeps dateadd -30min, sed -E and join-lines the arguments and keywords they are, and it is why $1 +5 is refused with a message saying to space it.

Arithmetic does not make a query a pipeline: swift + xcode is still a search, because a + in the middle of a sentence is far likelier to be one than a sum. A source is what starts a pipeline, as always.

Numbers are read and written plainly — a . for the point, nothing between the thousands, a leading - for a negative — and text that is not a number is an error rather than a guess: 12kg, $4.99 and an empty clipboard all fail and leave the text alone, as does dividing by zero. The arithmetic is decimal, so 0.1 + 0.2 is 0.3; division is carried to ten decimal places and rounded there.

Joining text

++ is the fifth operator and the one that is not a sum: it joins what follows onto the end of the text, with nothing between.

$1 ++ "!"
$1 | ++ "!"              ← the same thing, since ++ is an ordinary filter
$1 ++ " — " ++ $2        ← two clipboard items with a dash between them
$1 | trim ++ $(| strftime %F)

The words after it are the text to join, the way a date format is, so quotes are needed only to keep the spaces at either end — $1 ++ and more needs none. It needs a space either side like the others.

True and false

is and matches answer true or false, and four more work on those answers — what a trigger's pipeline condition is built from:

| language | !is en                       ← not: true becomes false
| !is fr && is es                         ← and: both
| language | is fr || matches ^Bonjour    ← or: either
| matches ^http ^^ matches \.pdf$         ← xor: exactly one

from answers about the entry rather than the text: whether it was copied in one of the apps you name, by name or bundle ID — | from Safari, $3 | from com.apple.mail. It can come anywhere in a pipeline.

! glued to the front of a step runs it and turns its answer round: !is en, !matches ^http. On its own after a | it turns round whatever it is handed — | language | is en | !.

&&, || and ^^ are operators like +: a space either side, and what follows is the other side. When that side starts with a filter — is es, !matches x — it is one step, run on the text the pipeline started from, not on the true or false to its left. That is what lets | language | is fr || matches ^Bonjour match the copy rather than fr. For a longer test use a $( … ), whose leading | reads the same text: && $(| headline | matches invoice). A value — true, $ok — is read as a value.

Both sides have to be true or false; anything else is an error rather than a quiet false. false && … and true || … are decided before the right side is looked at, so its $( … ) does not run — put a cheap test on the left of a slow one. And as with sums there is no precedence: a || b && c is (a || b) && c, and a $( … ) is how to group the other way.

Commands inside commands

$( … ) is a pipeline written inside another one. It runs on its own and stands for the text it produced, wherever a value goes — an argument to a filter, or the source the outer pipeline reads from.

$1 | trim ++ $($2 | dateadd +2min | strftime %m/%d/%y)
$($1 | trim) | upper                    ← as the source
$greeting ++ $($($1 | trim) | upper)    ← and they nest

The parentheses hold the whole of the command inside them: its | does not split the pipeline around it, its whitespace does not split the argument it is part of — so no quotes are needed around one — and a > inside is that command's own redirect. Parentheses in the text inside have to balance or be quoted.

A leading | inside means what it means outside. A > $name inside still writes that variable, and the substitution stands for the text that went into it. A forget inside keeps the run out of the history and drops the entry that command read from, which need not be the one the outer pipeline is reading.

A $( with no ) yet is not an error — it is a command still being typed, so nothing is previewed until it is closed, and the rows below the bar go on offering the filters and variables for the command inside the parentheses, which is where the caret is. Right after the ( the rows are the ones an empty bar shows: the whole clipboard history, and the variables and actions a word matches. Choosing one writes it inside the parentheses and nowhere else.

Sending the result somewhere

A pipeline may end with > $name, which stores the result in that variable — keeping the old value in the variable's history — instead of putting it on the clipboard, the way a shell redirect writes to a file instead of the terminal. The clipboard is left alone.

>> is the same redirect, writing to the end of what is there rather than over it.

$1 >> @shopping            adds the newest copy to the end
$1 | split , >> @shopping  adds each of its items
$1 | split , > @shopping   the list becomes those items
$1 >> $notes               joins the text onto the end of a variable

A name that is not a list yet is made into one. What is added is the value the pipeline produced: one item for plain text, however many lines are in it, and each item where the result was itself a list — split is how several are added at once.

The destination has to be a variable or a list. > $1 is refused, because a clipboard position is where values come from rather than somewhere they can be written; so is a bare > with nothing after it. A > inside quotes is an ordinary argument.

Both arrows keep what was there in the entry's history, like any other change to a variable, so @shopping | undo 1 is the list as it was before the last thing was added.

Lists

A list holds several things under one name. It is written @name, and it is the same entry $name is: @shopping is the items, $shopping is the text they make. That is the whole of the relationship, because a list is text with a shape — its text is its elements, one to a line. Every other filter goes on reading a list as the text it looks like, so sort-lines and unique-lines need no list-shaped twin.

Four things produce one:

@shopping                     a list by name
@<$1, $greeting, and this>    a list written out
$1 | split ,                  the text, divided
⌘↩, rows picked, ⇥           which writes @<$2, $5> into the bar

@<…> holds its elements as they were typed and expands them when the command runs, so @<$1, $2> names two things rather than spelling two. Only commas at the top level divide it. A trailing comma is room left for the next item rather than an empty item, which is what lets @<a, > be a list of one. @<> is the empty list.

Lists inside lists

An item is usually a piece of text, but it can be a list or a hash. An element of @<…> written as a whole list or hash — @tags, %home, @people[0], or another @<…> or %<…> — keeps its shape:

@<@<a, b>, c>                 a list, then c
@<%<name: Ada>, %<name: Bea>> a list of two hashes

Anything else in @<…> is text, as it always was: $1 and $( … ) stand for the text they produce. A name that names nothing is left as written, so @<@handle> is still the text @handle if you have no list called that.

Nested items read as their text wherever text is wanted — the list above is a, b, c a line each — so every filter that deals in text goes on working. What keeps the shape is picking an item out: @people[0] and item 0 hand back a hash when the item is one, and map hands each item to its filter as the thing it is.

An @ starts a command the way a $ and a | do, but only at the front: an @ in the middle of a query is an address or a handle far more often than it is a list.

One item, @name[0]

Brackets on the reference take a single element. They count from zero — [0] is the first — and a negative counts from the other end:

@shopping[0]              the first item
@shopping[1]              the second
@shopping[-1]             the last one
@shopping[1] | upper      a value like any other from there on

Everything that indexes a list counts from zero — the brackets, item, index, the position on an action, and the numbers beside a list's items in the Variables pane. So @shopping[1] and @shopping | item 1 are the same element, and -1 is the last one everywhere. Ask for an item the list does not have and the error says where the numbering runs — There is no item 3: the list holds 3, numbered 0 to 2 — because reaching for 3 and meaning the third is the easy mistake, and on a longer list it would quietly hand you the fourth instead.

@shopping[1] and @shopping | item 1 are two ways of writing one request, and either gives you the item's text rather than a list holding it. The brackets are worth reaching for when the item is what a command starts from, where the filter form reads backwards: @shopping[-1] ++ "!" says it in the order you thought it.

Plain text is read as its lines here as everywhere, so @notes[1] is the second line of an ordinary variable. A name that is no list at all is a different error from an index the list does not reach — the two are told apart rather than run together.

Three details worth knowing:

  • The -1 is an index, not a sum. Elsewhere $1 -5 is arithmetic written without its space and the bar says so; inside the brackets nothing is, because they have already said what is in them. Written after them it still is, so @shopping[1] + 5 needs its spaces like any other sum.
  • @{name} names a list outright. If you have a variable actually called sizes[2], @{sizes[2]} reaches it — braces mean the whole of this is the name, which is what they are for in ${last-1} too.
  • An item that is there can be written over. "tea" > @shopping[1] replaces the second item. Writing past the end is an error rather than a new item — >> is how a list grows.

Paths: @people[0]{name}

Lists and hashes nest, so a reference can take any number of steps into one — [index] into a list, {key} into a hash, in whatever order the value has them:

@people[0]{name}            the first person's name
%order{items}[-1]{sku}      the sku of the last line of an order
%order{items} | map fill "{{qty}} × {{sku}}"

The sigil says what you start from — @ a list, % a hash — and each step's brackets say what it expects. Get one wrong and the bar says which and how to write it: %order{items} is a list — reach into it with [0], not {sku}.

A path is somewhere to store something too:

"9" > %order{items}[0]{qty}      change a value inside
$1 > @people[0]{phone}           add a key to the first person
%<sku: C3, qty: 4> >> %order{items}   add a line to the order

A missing key is made; an index has to exist already.

An unfinished @name[ is a command half typed rather than one spelt wrong, so nothing is previewed and nothing is complained about until you close the bracket.

Text that meets something wanting a list is read as its lines, so $1 | join runs a column into a sentence without splitting it first. The coercion goes that way only. A list sent to a $name is an error, named where it happens and shown in the bar before ↩ — a variable holds one thing, and keeping one item while losing the rest is not something to do quietly. There are three ways to say which one thing you meant, and each of them says it out loud:

@shopping | join > $line     all of them, run together
@shopping | first > $next    one of them, chosen
$shopping > $line            the same entry read as text

Running a filter over every item

map runs another filter over each element and hands back a list of what each one gave. Its first argument is the filter to run; anything after that belongs to that filter:

@recipients | map upper
@recipients | map trim
| map @recipients upper       naming the list instead of handing it over
$1 | split , | map trim

The result is always as long as the list that went in — one answer to one item, which is the whole of what mapping means. A filter that produces a list of its own therefore lands as a single item that is that list, rather than melting into the items around it: @<a;b, c;d> | map split ; is a list of two lists.

Each item is handed to the filter as the thing it is, so a list of hashes can be mapped through a template — @people | map fill "Hi {{name}}" — and a list of lists through item.

An operator has to be quoted, because a pipeline splits its steps on one: write map "+" 5 and map "++" "!", not map + 5. The same goes for &&, || and ^^.

The filter it exists for is eval. A list of templates becomes a list of what they say:

@letters | map eval
@rows | map eval > @filled

Text is read as its lines here as everywhere, so $1 | map trim tidies a column and gives you a list of it.

Working on whole items

These work on a list's items — whole, whatever each one is — where sort-lines and friends work on lines:

@lines | filter ^ERROR          keep the items that match a regex
@lines | reject "^(DEBUG|INFO)" drop them — a | in a pattern needs quotes
@names | sort                   file2 before file10
@emails | unique                each once, the first kept
@people | sort-by name          a list of hashes, by one key
@<@<a, b>, c> | flatten         a, b, c
@results | take 5               the first five; take -5 the last
$1 | split | drop 1             without the header line
@headers | zip @values          keys and values into one hash
$1 | split , | sum              and avg, min, max

Each reads the list at hand or one named first — | take @results 5 — the way item and map do.

A list on the clipboard

When a command's result is a list, the first item goes on the clipboard and the rest wait behind it. Each paste puts the next one there, so a row of values goes into a row of fields with ⌘V and nothing else — which is what $1 | split , is for. The HUD says which item is up.

Copying anything new interrupts it: a fresh copy is a fresh intention, and a queue that went on feeding items into whatever came next would be the app arguing with you. Reaching the end ends it too, and starting another list replaces whatever was left of the first.

Gathering copies into one list

You do not have to write a command to get a list. ⌘⌥A starts gathering: every copy you make from then on is kept in one list, until you press it again.

Pressing it again does not just stop — it puts the gathered list on the clipboard, first item first. So the whole thing is:

⌘⌥A          start gathering
              copy, copy, copy — from anywhere, in any app
⌘⌥A          stop, and the first one is on the clipboard
⌘V ⌘V ⌘V     the rest follow, one per paste

Everything in this section applies to it from there — interrupting, continue, index.

The gathered list is @_

What you gathered is kept in a list called @_, so it does not disappear the moment you copy something else. It is an ordinary list, which means:

@_ > @shopping     keep this gathering under a real name
continue @_        pick it up where you were interrupted
@_ | first         read it without spending it

That is the difference between a stack you get one go at and one you can keep.

Three more things worth knowing:

  • Your copies are kept as normal too. The list is as well as the history, never instead of it, so abandoning a gathering half way through loses nothing.
  • Starting a new gathering replaces @_. The items are still in your history; it is the list that starts fresh. If a gathering was worth keeping, send it somewhere with a name before you start the next one.
  • One run keeps 50 copies, and says so when it reaches that. It also refuses to start while recording is paused, since nothing would be gathered.
  • Stopping does not paste anything by itself. You press ⌘⌥A while still in the app you were copying from, so an automatic paste would land the first item right back in it. The list goes on the clipboard and waits for your ⌘V.

Because gathering has no window of its own, three places show it is on: the HUD when it starts and stops, a banner across the top of the overlay with the count, and the menu bar item, which reads Paste 4 Gathered.

⌘⌥A can be changed in Preferences → General.

Reaching for the list again starts it over, from the first item. Carrying on is the rarer of the two, so it is the one that has to be said out loud:

continue @shopping     what has not been dealt yet
index @shopping 3      from the fourth item on

Being interrupted is the ordinary way to end up half way down a list, since something has to be copied to fill in the field beside it, and continue is how the rest of the form gets filled. What it hands over is the tail, so a second continue finds where the first left off.

Stepping the queue does not record anything: the list is already in the history as one entry, and nine entries for nine items would be the queue emptying the history it lives in.

A paste is recognised by watching for ⌘V, which is the only signal macOS offers. That needs the same Accessibility permission automatic pasting needs. Without it a list still lands on the clipboard first item first; it simply does not step on its own, and the rest of it is still in @name to be reached for.

Which of the two just happened is said in the HUD every time a list is dealt out: either the rest follow as you paste, or the rest need Accessibility permission to follow as you paste. The keyboard is watched only while a list is being dealt out, so granting the permission takes effect on the next list — there is no need to restart the app.

Hashes

A hash holds values under keys. It is written %name, and like a list it is the same entry $name is: %contact is the keys and values, $contact is the text they make, written out as key: value lines.

%contact                    the whole hash
%contact{email}             what the key email holds
%contact{address}{city}     a key inside a key
%contact{email} | upper     a value like any other after that

A key holds text, a list, or another hash, and reading one gives back that thing: %contact{tags} is a list, %contact{address} is a hash. The text of a hash writes each of them out:

name: Ada
email: ada@example.com
tags:
  - red
  - blue
address:
  city: London

A key is whatever is between the braces, spaces at either end aside, so %contact{first name} is one key. As an argument to a filter, quote the whole reference if the key has a space in it: fill "%people{ada lovelace}".

Writing one out

%<…> is a hash written out, the way @<…> is a list:

%<name: $1, email: $2>
%<tags: @<red, blue>, address: %<city: London>>
%<name: $1, email: $2> > %contact

Pairs are divided by commas at the top level and each pair at its first colon, so a value may hold one — %<url: https://example.com> — and a key that must hold one is quoted: %<"a:b": 1>. The values expand when the command runs, one at a time, so a comma or a > in what $1 holds is part of the value and not of the hash. A value written as a whole list or hash keeps its shape, as it does in @<…>. A trailing comma is room for the next pair, and %<> is the empty hash.

Building one

A key is somewhere to put something, so a hash is built by sending values to its keys:

$1 > %contact{email}             the newest copy, under email
@tags > %contact{tags}           a list, under tags
%home > %contact{address}        a hash, under address
$1 > %contact{address}{city}     making address if it is not there yet

The hash is made the first time a key is written, along with any hash on the path to it. What else it holds is kept, in the order it was written.

> and >> mean what they mean everywhere else:

%other > %contact           the hash becomes that one
%other >> %contact          its keys are written over these; the rest stay
$1 >> %contact{tags}        one more item on the end of a list
"!" >> %contact{name}       text joined on the end of text

Text that is key: value lines is a hash wherever one is wanted: %name reads a variable typed in that way as one, and "a: 1" > %fresh makes one. Text that is not such lines is refused rather than guessed at, and so is a key written inside something that is not a hash — %contact{name}{first} when name holds text.

A hash sent to a $name is refused, the way a list is: a variable holds one thing. Write it to a %name to keep its keys. A secret cannot be a hash.

Templates: fill

fill reads the text it is handed as a template. Every {{key}} in it is replaced by what that key holds in the hash named after it:

$letter                        Dear {{name}}, re: {{ref}}
$letter | fill %contact        Dear Ada, re: INV-204
"Hi {{name}}" | fill %contact  Hi Ada
@letters | map fill %contact   a list of templates, filled
  • Spaces inside the braces are ignored: {{ name }} is {{name}}.
  • A key holding a hash is reached with dots: {{address.city}}.
  • A key holding a list fills in as its items, one to a line.
  • A key the hash does not have is left as written, braces and all, so a template can be filled in stages.
  • What goes in is not read again, so a value with {{ in it stays as it is.

The hash can be written out on the spot — $letter | fill %<name: $1> — or be the value at hand, with the template as the argument instead:

%contact | fill "Hi {{name}}"
@people | map fill "Hi {{name}}"     one line for each hash in a list

An argument starting with % always names the hash, so the two ways round never get confused.

Asking for the rest: --ask

fill --ask puts up a small form for the placeholders the hash doesn't answer, and fills them in from what you type:

$letter | fill --ask %contact
$letter | fill --ask              no hash: every placeholder is asked for

The form has a field for each missing key, in the order the template uses them. Tick Save as hash to keep the answers: they are written into the hash that was named — %contact here — or into whichever name you give, so the next fill from it has nothing left to ask. For a hash written out or one in hand, the name starts empty. The form looks like the overlay and works like it: ↩ fills, ⇥ moves to the next answer, ⌘S ticks Save as hash, and esc cancels — which leaves everything as it was.

It asks only when you asked for the run: ↩ in the search bar, an abbreviation you typed, an action run from the menu bar or a shortcut. The preview above the bar never asks — it shows {{ref}} still unfilled until you press ↩. A trigger fires because something was copied, which isn't asking, so there --ask fills what it can and leaves the rest as written. Write --ask-in-triggers for a trigger that should put the form up anyway.

A key is asked for once however often it appears, and once for a whole list — @letters | map fill --ask %contact asks for {{date}} a single time.

Taking a hash apart

%contact | keys           name, email, tags
%contact | values         Ada, ada@example.com, [red, blue]
%order | to-json          back out as JSON, keys in order

to-json writes text that is exactly a number, true or false bare, so JSON read with json-hash goes back out as it came in.

From a spreadsheet: tsv and csv

Copy a range of cells with its header row, and tsv makes each row below it a hash keyed by the headers — which, with map fill, is a mail merge:

$1 | tsv > @rows
@rows | map fill "Hi {{Name}}, your order {{Order}} has shipped."
@rows[0]{Email}

csv is the same for comma-separated text, and csv ; for semicolons. Quoted cells may hold commas and line breaks. Every row gets every column — a missing cell is empty — and --no-header gives a list of lists instead.

From JSON: json-hash

json-hash reads the text as JSON. An object becomes a hash, with its keys in the order they were written, and an array a list — so JSON full of objects becomes lists of hashes, all reachable by key:

$1 | json-hash > %order
%order{customer}{name}
%order{items} | map fill "{{qty}} × {{sku}}"

Numbers keep the characters they were written with (1.50 stays 1.50), true and false are those words, and null is empty text. JSON with an array at the top is a list; a bare string or number, or text that is not JSON, is refused.

fill knows nothing about $name. eval is the filter that expands those, and the two chain: $t | fill %h | eval.

Running one

Substitute filters, arithmetic and the date filters preview live as the pipeline is typed: they are pure and instant. Script filters do not, because a half-typed argument would be run for real; the bar shows the text as far as it got and which filter is waiting. ↩ runs the whole pipeline including scripts, and the result becomes the clipboard contents and the newest entry in the history — or goes to the variable named by a trailing > $name.

A script may opt out of that caution with Run while a search-bar pipeline is being typed in the Actions pane, which is off by default and worth turning on only for scripts that are fast and change nothing.

What ↩ pastes is what the preview showed. A live script is not run a second time for the same text, so uuid pastes the UUID you were looking at, and it stays the same while you go on typing — uuid | upper upper-cases that one. The next time you open the bar, you get a new one.

Typing just uuid is a search, but the top row is a new UUID, marked Runs the uuid action; ↩ pastes it. now does the same with the time. Any action marked works without input does it.

↩ only runs once there is nothing left to complete: while a name is still being typed — a filter's, or an entry's — it adds the highlighted one instead, so ↩ ↩ is type-a-few-letters then run. A reference that already names something is settled, so $1 and ↩ still puts the newest item on the clipboard.

What the rows offer while you type

While a pipeline is being typed the rows below the bar list whatever the end of it is asking for, and ⌘1–⌘9, ↩ or ⇥ fills the highlighted one in.

  • Where that is a filter — the step after the last | — the rows are matching filter names. A leading | reads the newest clipboard item, so there is no source to name and those rows are filters from the first keystroke.
  • Where it is a $, they are matching variables, by name alone. Contents are not searched and clipboard items are not listed: a pipeline reads those by position. Three places take one — the source, a $… argument, and the destination after a >.
  • Where it is a @, the same three places offer the lists instead. A @ lists only the lists, since offering a variable there would be offering a name that reads as something else, while a $ offers everything.
  • Where it is a %, they offer the hashes — the ones a command made, and variables whose text is key: value lines. Once a [ or { follows the name, you are writing a path and the rows stop offering names.

A bare $ lists no secrets, and $! lists nothing else. The ! is a second prompt, asked on purpose: mixing the two would put the secrets in front of everyone who typed a $.

Taking a row replaces only the reference being typed, leaving the step it belongs to and everything before it alone: $1 | join-lines $gre becomes $1 | join-lines $greeting.

The destination after a > is the one place ↩ does not finish a name, since a name that matches nothing is the ordinary case there. Its rows are an offer that ⇥ or ⌘1–⌘9 takes. The sigil need not have been typed yet — > tidy is a destination written wrong, and the names offered there are how it gets written right.

Command history

A pipeline that runs without error is remembered, without its source: only the chain of filters is kept, because the same chain is usually wanted again against something else. The last 100 are kept, in shell-history.json.

↑ at the top of the list walks back through them, the gesture a shell has at an empty prompt: the first press puts the most recent command in the bar, each one after that goes further back, and ↓ comes forward again. Typing ends the walk. A command comes back written behind whatever source the bar already holds, so ↑ with $3 typed offers $3 | trim | upper.

The walk is offered only with the bar empty or holding a command: a search half typed is a different thing to be in the middle of, and writing a pipeline over it would lose the words you were still looking for.

Once a | has been typed, the bar offers the most recent past chain that carries on from what is written so far, drawn in grey past the caret. ⇥ takes it; failing that ⇥ fills in the highlighted filter name. Since ⇥ also starts a command from a row, ⇥ ⇥ goes from a highlighted clipboard item to that item piped into the last filter chain that was run on anything.