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, when there is a $ inside it, or when it is sent somewhere — "hello" >> @people adds hello to the end of a list. 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.
A path written straight after a $name is the text of the item it reaches: $shopping[1] is the second item, $contact{email} one key of a hash, and "Dear $people[0]{name}," puts one inside other words. That is the difference from @shopping[1], which is the item as it is — a list or a hash stays one — and which a filter reads as the thing it names rather than as text. A path that reaches nothing leaves the brackets as text after the $name, so matches ^$prefix{3} is still a regex; ${name}[1] keeps them as text always.
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.
A backslash escapes the quote you are inside, and itself: "say \"hi\"", 'it\'s', and "C:\\" for text that ends in a backslash. Any other backslash stays as written, so \d in a pattern still means a digit. You can also use the other kind of quote and need no escape: 'say "hi"'.
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.
A pipeline action gets its arguments as a list, @ARGS, counted from zero like every list. Saved as greet, the pipeline "Hello $ARGS[0]" makes greet Ada into Hello Ada, and an action that fills a template list with one word looks like this:
@csr | map fill %<domain: @ARGS[0]> saved as make-csr
make-csr payments {{domain}} becomes payments
- No arguments is an empty
@ARGS, not an error;@ARGS[0]then says there is no item 0. $ARGSis all of them as text, one to a line.$ARGS[0]is the first one as text, for an argument or quoted text — asgreetdoes above, and as passing one on to another action does:| other $ARGS[1]. An action named inside another gets its own arguments, never its caller's.@ARGScannot be written to from inside the action, and outside oneARGSis an ordinary name you can use for a variable of your own.
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 (with or without .app) or bundle ID — | from Safari, | from Xcode.app, $3 | from com.apple.mail. It can come anywhere in a pipeline, and it asks about a picture's entry as readily as any other. from-window does the same for the title of the window it was copied in: $3 | from-window invoice.
! 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.
Stopping a pipeline
A test with another step after it decides whether that step runs. false stops the pipeline right there and nothing changes: no clipboard, no variable, no tag. true hands the next step the text the test was handed, not the word true.
| is-kind image | ocr ← read a picture; leave text alone
| matches ^https?:// | remove-url-tracking ← only touch links
| is-kind rtf || from Pages | unfmt ← either, then plain text
A test at the end of a pipeline still answers true or false, and so does one followed by !, &&, || or ^^. Your own pipeline action counts as a test when its last step is one. A test that stops a pipeline action also stops the pipeline that ran it. In the search bar the preview says Stops at … before you press ↩. A trigger whose step stops leaves the copy as it was, as if it hadn't matched.
What kind of thing it is
is-kind asks what you're holding: image, files (a copy in Finder), rtf (formatted text, RTF or HTML) or text (plain text). Name more than one to accept any of them: is-kind text rtf. While the text is still what you copied, it's the copy's kind. Once a step has rewritten it, it's text. It is the one test that can start from a picture, so | is-kind image | ocr works with a screenshot on the clipboard.
kind says the same thing in the clipboard's own words: the pasteboard types the value holds, as a list, in the order an app pasting it reads them — public.rtf, public.html, public.utf8-plain-text for formatted text, public.png for a screenshot. A copy out of Excel or Keynote lists its types in the order that app offered them, which is the app's choice: Excel puts public.html first and its own com.microsoft.… types last, so | kind | filter com.microsoft picks out Office's own. It lists only what the history kept, and it is how to find a type to add under Ignored Types. Like is-kind it can start from a picture, and from an app's object too.
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, a list, a file or a clipboard position; a bare > with nothing after it is refused. A > inside quotes is an ordinary argument.
To a clipboard position
> $2 puts the result into the history as a new entry at $2, and what was $2 and everything below it moves down one — a way to make an entry without copying anything first:
"call the bank" > $1 a new newest item, on the clipboard too
$3 | trim > $2 a tidied copy of $3, just under the newest
> $1 puts it on the clipboard as well, since $1 is what the clipboard holds; anywhere lower, the clipboard is left alone. It takes one piece of text, formatted or not, and goes at most one past the end of the history.
A list becomes an entry per item, the first at the position and the rest below it in order — so split makes one entry per line:
$1 | split > $2 each line of $1 its own entry, from $2 down
$1 | split , | map trim > $1 each item, the first on the clipboard
Blank items are left out and a repeat goes in once; the copy you split stays where it was. More than ten entries asks before it goes ahead, except from a trigger. Text the history already has is moved there rather than kept twice. >> doesn't work with a position — ⌘E is how a row is changed. ⌘N in the overlay does the same as > $1 with an editor.
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.
To a file
A path after the arrow writes the result to a file instead — a path starting ~/, / or ./:
$1 | json > ~/Desktop/data.json writes data.json, over one that is there
$1 >> ~/notes/log.txt adds the copy to the end, as a line of its own
$1 > ~/Desktop/ a folder: the file is named for what is in it
$1 > ./today.txt ./ is the save folder in Preferences → General
The two arrows treat a file that is already there the way a shell does:
| Arrow | A file already there |
|---|---|
> | Written over: the file holds the new result, and what it held is gone |
>> | Added to: the text goes on the end, on a line of its own, as echo text >> file would put it. A file that is not there is made |
There is no >|, since > already writes over a file. A path that is a folder is the exception: the app chose the name, so it never writes over anything there, but chooses a name that is free, numbering it as Finder numbers a second copy — notes.txt, then notes 2.txt.
⇥ finishes a path starting / or ~/, as it does in a shell: $1 > ~/Desk becomes $1 > ~/Desktop/, and another ⇥ goes on into the folder. While you type a path, the list underneath shows what's in the folder that matches. Where several names match, ⇥ first fills in what they have in common; press it again to go through them one at a time — each goes into the bar and its row lights up — and ⇧⇥ to go back. ⌘1–⌘9 or a click picks one directly, and picking a folder shows what's inside it. Keep typing to settle on the one you're on. ↩ always writes to the path as it reads in the bar. Hidden files are offered once you type the ..
The path runs to the end of the command, spaces and all — > ~/My Notes/today.txt needs no quotes — and folders on the way are made. ~ is your home folder. ./ is the save folder chosen in Preferences → General, your Desktop unless you choose another: the app has no folder of its own for a path to be relative to.
What is written is the result as it stands:
- Text is written as UTF-8. A path ending
.rtfor.htmlkeeps the formatting the text was copied with, when it has some. - A picture is written as the image file it is, and converted when the path asks for another format:
$1 | resize 50% > ~/Desktop/small.jpg. Writing a picture to.txt, or text to.png, is refused. - A list is written as its lines, and a hash as its
key: valuelines — where a variable that holds one thing would refuse them. - A folder — a path ending in
/, or one that already is a folder — gets a file named for its first line, or for the site a link is on, with the extension that suits it:.jsonfor JSON, the language's own for code (.swift,.py,.sh…),.rtffor text copied formatted,.txtotherwise, the image's own for a picture.
>> adds text only. A picture, or a file that is not text, has no end for more of it to go on, and saying so is better than a file nobody can open.
The clipboard is left alone, as it is for a variable, and the HUD names the file that was written — the numbered one, when a folder's file had to be numbered.
A secret goes to a file like anywhere else: $!token > ~/.config/tool/token writes it while the vault is open, asks for Touch ID when it is shut, and the preview shows the mask rather than the secret. Bear in mind what the file is: plain text on the disk for as long as it is there — backed up, synced if it is in iCloud Drive, and readable by anything you run. A trigger that writes a secret to a file does so on every copy while the vault is open, without asking; while it is shut, the trigger does not fire.
The preview above the bar writes nothing: it shows what would go in the file, and the file is made on ↩. The same holds for a trigger or a pipeline action ending in a file — | upper >> ~/log.txt — which writes when it runs, not when you test it in Preferences. A rule added through the API cannot write a file until you approve it, and a command run through the API reports the write rather than making it.
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.
A picture, a file list or an object goes in as itself when it is named on its own — @<$1, $2> with a screenshot at $2 is a list holding the text and the screenshot. Pasting the list deals out the screenshot as a picture, and files as files. Filters that move whole items take one — reverse, take, drop, from, item, continue, restart, unique, tag — and so does join, which puts them together as one paste. A filter that reads text refuses a picture or an object by name rather than reading it as nothing; a file list reads as its paths. A variable keeps its own values, so > @name refuses a list with a picture or an object in it, and keeps a file list as its paths. 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 1are the same element, and-1is 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 for3and 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
-1is an index, not a sum. Elsewhere$1 -5is 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] + 5needs its spaces like any other sum. @{name}names a list outright. If you have a variable actually calledsizes[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
Joining pictures and formatted text
join over a list with copies in it makes one paste with each item as it was copied: text with its formatting, pictures inline, files attached. The text the command shows — and what an app that takes only plain text gets — is the text alone; a picture has none, so it is left out with the separator beside it.
@<$1, $2, $3> | join " / " text, a screenshot, more text — as one paste
A step after join that rewrites the text drops the formatting and the pictures, as any step does. In the overlay, ⌘V over rows picked out does the same with the separator set in Preferences.
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.
A list of pictures maps through image filters, one picture at a time — a list from the history or one a variable keeps:
@shots | map resize 50%
@<$1, $2, $3> | map crop | map png > @cropped
@screens | map ocr
@urls | map qrcode
The pictures are made when you press ↩, not while you type. A picture only goes to a filter that takes one, so a list mixing pictures and text is refused rather than half done.
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 gather is kept in a list called @_, so it does not disappear the moment you copy something else. It fills up as you copy, so you can read it or save it before you have pressed ⌘⌥A to stop. 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.
- A new gathering replaces
@_as soon as its first copy arrives. The items are still in your history, and the last gathering is one of@_'s past values; 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
⌘⌥Awhile 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, or cleared, in Preferences → General; without it, Gather Copies in the menu bar menu starts and stops gathering.
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.
The last list: @@ and restart
@@ is whichever list was last dealt out to the clipboard — a named one, a split, a gathering — so you do not have to remember what it was called:
@@ carry on where the last list left off
restart start the last list again from item 1
restart @shopping start a named list again
@@ | first the last list, read like any other
@@ > @kept keep it under a name
@@on its own carries on, exactly likecontinue @@. Anywhere else — after a|, insidecontinue, with>— it is the whole list.restartgoes back to the top. After acontinue, that is the top of the whole list, not of the partcontinuepicked up.- It is the whole list, and it stays. It is still there after the last item has been pasted, so
restartgoes round again. It is forgotten when PasteDaemon quits. - You cannot store anything in
@@. It is the app's to say;> @@is an error.
Pasting down the history: history-paste and stop
history-paste deals the history itself out a row per paste, starting from the place you name:
history-paste $2 $2, then $3, $4 …
$2 | history-paste the same thing
history-paste $4 --up $4, then $3, $2, $1
history-paste $3 5 $3 to $7, then it stops
history-paste from $1 down
| stop end whatever list is being pasted
- Down is newest to oldest, the order
split > $2puts a list in. Three copies made one after another sit at$3,$2and$1, sohistory-paste $3 --upputs them back in the order you made them. - Each row pastes as it was copied: a picture as the picture, formatted text formatted. Sealed rows are passed over.
- It runs until you copy something, run
stop, or it reaches the end of the history — or of the places you asked for:history-paste $3 5stops after$7, andhistory-paste $7 5 --upafter$3. A sealed row among them is skipped, not made up for. The rows are picked when it starts, so the history moving underneath changes nothing. Nothing new is added to the history: the rows are already there. stopkeeps your place. The next⌘Vpastes what is on the clipboard again, and@@carries on where the list stopped. While a list is being pasted, the menu bar menu has Stop Pasting the List too. Likepause-copying-for,stopruns only when you start it — from the search bar, a shortcut or the menu bar — never from a trigger or the API'srun.
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, another hash, or a picture, and reading one gives back that thing: %contact{tags} is a list, %contact{address} is a hash, and %brand{logo} — put there with $1 > %brand{logo} — is the picture, which pastes as itself and goes through image filters like any other. 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.
Choices
A placeholder can offer a few answers to choose from, written after a colon:
{{signoff: Regards | Thanks | Cheers}} choose one of these
{{who: @people}} one of a list's items
{{link: $clip}} one of your latest nine copies
{{?: Hi | Hello | Hey}} one drawn at random
In the form, the field starts on the first choice; ↑ and ↓ step through the rest, the menu at the end of the field lists them all, and you can still type something else. The answer is kept under the name before the colon, so writing {{signoff}} again further down repeats it without asking twice:
"{{signoff: Regards | Thanks}},\nArmand\n\nP.S. {{signoff}} again!" | fill --ask
When nobody is asked — the preview, a trigger, the API, or fill without --ask — a choice is its first option, so the text still comes out finished. A hash that has the key answers it first, as it does any placeholder.
Optional sections
A part of the template can be a checkbox on the form: everything between {{[x] name}} and {{/name}} is kept when it is ticked and left out when it is not. Write [x] for one that starts ticked and [ ] for one that starts unticked:
"Hi {{name}},{{[ ] ps}}
P.S. {{note}}{{/ps}}" | fill --ask
On the form, space ticks or unticks it. A section can hold anything a template can — several lines, other placeholders, other sections — and when it is left out, everything inside it goes too. When nobody is asked, it is as written to start. A hash can answer it: yes keeps it, and no, false, 0, off or nothing leaves it out. Save as hash saves yes or no.
{{?: …}} is never asked: each one draws again every run. In the search bar the preview shows the one that ↩ will paste. A secret list (@!name) offers no choices, and neither do sealed copies for $clip; the field is then left for you to type into.
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
%order | to-json | env as .env lines: ID=7, CUSTOMER_NAME=Ada
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. env writes a JSON object as KEY=value lines, flattening an object inside into its keys.
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.
↩ runs the command as you wrote it, and ⇥ completes, as in a shell: while a name is still being typed — a filter's, or an entry's — ⇥ fills in the highlighted one, so ⇥ ↩ is type-a-few-letters then run. ↩ never swaps what you typed for a suggestion: $2 runs as $2 even with a list called @list2. A filter's name typed whole lists the longer names beginning with it under it: $1 | json offers json-compact and the rest, and $1 | describe offers describe-image. ↩ runs json; ↓ then ⇥ takes the longer one.
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 iskey: valuelines. 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 one place ↩ fills in a name is where nothing follows the sigil: a bare > $, or a bare # after tag, names nothing to run, so ↩ takes the highlighted row there and ↩ again runs. That is how the command ⇧↩ writes finishes with ↩ ↩. 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. A command with no filter keeps its arrow instead: $1 > ~/Desktop/notes.txt is remembered as > ~/Desktop/notes.txt, so the next copy can be written to the same place. 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, or $3 > ~/Desktop/notes.txt.
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. Moving the highlight down to another filter sets the grey offer aside, so ↓ ⇥ takes the row you chose. 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.