Skip to content

Shell Script actions

A classic Shell Script action runs a shell script, either directly or from a file. The script can be written in any language that can be executed from the command line, such as Zsh, Python, Ruby, Perl, etc.

Running a shell script from JavaScript

JavaScript actions can call shell scripts using the $ syntax:

javascript
// #popclip shell js example
// name: Print in Uppercase
// entitlements: [script]
const printMe = popclip.input.text.trim().toUpperCase();
const { stdout } = await $`lp <<< ${printMe}`;
// e.g. "request id is Office_Printer-294 (1 file(s))"
const requestId = stdout.match(/request id is (\S+)/)?.[1] ?? "unknown";
popclip.showText(`Printing: ${requestId}`);

See Calling external scripts for the full story.

Submitting to the directory

Extensions submitted to the Extensions Directory should use JavaScript actions in preference to Shell Script actions. A submission with a Shell Script action must include a shellScriptRationale in its Config.

Properties

A Shell Script action is defined by a code snippet that specifies an interpreter in its config header, or starts with a #! line. Alternatively, a config snippet may define a shellScript or shellScriptFile field, as follows:

KeyTypeDescription
shellScriptStringA string to be run as a shell script. The string will be passed via standard input to the specified interpreter, invoked without arguments.
shellScriptFileStringThe name of a file in the extension's package directory. See Shell script file execution for more details.
interpreterString (optional)Specify the interpreter to use for shellScript or shellScriptFile. You can specify a bare executable name, for example ruby, and PopClip will look for it in the PATH of the user's default shell. Alternatively, you can specify an absolute path such as /bin/zsh.
stdinString (optional)For script specified as shellScriptFile only. Set the name of a script variable to pass via standard input (stdin). If omitted, no standard input is provided to the script.
shellModeString (optional)How the script is executed: login (the default), nonlogin or none. See Shell mode.

Shell script file execution

The shellScriptFile will be executed as follows:

  • If an interpreter is specified, then PopClip will call this interpreter with the script file path as argument.
  • Otherwise, if the script file has executable permissions set (with chmod +x) and the first line of the file starts with #!, then PopClip will execute the file directly.
  • Otherwise, if the extension has a popclipVersion and it is set to a value less than 4035, or if the script file name ends with .sh, the script will be executed with /bin/sh. (This behaviour is for backward compatibility with existing extensions.)
  • If none of the above conditions are met, the extension will fail to load because no interpreter has been specified.

The current working directory will be set to the package directory.

Shell mode

The shellMode field controls how the script run is executed:

  • login (the default): via the user's default shell as a login shell (-l), so the script sees the user's usual PATH and profile environment.
  • nonlogin: via the user's shell without -l — for environments configured in .zshenv alone, without profile side effects.
  • none: no shell at all — the interpreter (or the executable script file itself) is executed directly, with a minimal environment (PATH=/usr/bin:/bin:/usr/sbin:/sbin plus the POPCLIP_* variables). Fastest and most predictable.

Input and output

Within the script, access the selected text with the shell variable POPCLIP_TEXT. Many other variables are also available, as listed in Script variables.

Optionally, the script may read from standard input (stdin). If the stdin field is set, the script will receive the contents of the specified variable via stdin. For example, if stdin is set to text, the script will receive the contents of the POPCLIP_TEXT variable via stdin.

Any text returned by the script via standard output (stdout) will be available to the after step.

Indicating errors

Shell scripts should indicate success with an exit code of 0, and should indicate failure with a non-zero exit code. On failure, PopClip will display the shaking-'X'.

Scripts may signal that there is an error with the user's settings with specific error code 2. In this case, PopClip will pop up the extension settings UI.

Examples

Package example

The Say extension demonstrates a packaged shell script extension.

Snippet examples

About these examples

The examples are given as code snippets.

Examples of passing the selected text to the say command to be spoken aloud:

zsh
#!/bin/zsh
# #popclip
# name: Say (variable)
say $POPCLIP_TEXT
zsh
#!/bin/zsh
# #popclip
# name: Say (stdin)
# stdin: text
say
zsh
#!/bin/zsh
# #popclip
# name: Say (option)
# stdin: text
# options:
# - { identifier: voice, type: string, label: Voice, defaultValue: Daniel }
say -v $POPCLIP_OPTION_VOICE

Some examples of returning a string back to PopClip via stdout, in different languages:

zsh
#!/bin/zsh
# #popclip
# name: Helloworld in zsh
# after: show-result
echo -n "Hello, ${POPCLIP_TEXT}!"  # `-n` for no newline at end
python
#!/usr/bin/env python3
# #popclip
# name: Helloworld in python
# after: show-result
import os
print('Hello, ' + os.environ['POPCLIP_TEXT'] + '!', end='')
# `end=''` for no newline at end
ruby
#!/usr/bin/env ruby
# #popclip
# name: Helloworld in ruby
# after: show-result
print 'Hello, ' + ENV['POPCLIP_TEXT'] + '!'
perl
#!/usr/bin/env perl
# #popclip
# name: Helloworld in perl
# after: show-result
print "Hello, $ENV{'POPCLIP_TEXT'}!\n";
swift
#!/usr/bin/env swift
// #popclip
// name: Helloworld in swift
// after: show-result
import Foundation
let text = ProcessInfo.processInfo.environment["POPCLIP_TEXT"]!
print("Hello, \(text)!")

A more substantial example:

Example snippet: Download an Iconify icon as SVG
zsh
#!/bin/zsh
# Download an Iconify icon to Downloads folder as SVG
# Example input: simple-icons:vivaldi
#
# #popclip
# popclip version: 4050
# name: GetIcon
# regex: ([a-z0-9]+(?:-[a-z0-9]+)*):([a-z0-9]+(?:-[a-z0-9]+)*)
# stdin: text
# after: copy-result
#
set -e # exit on errors
eval "$(/opt/homebrew/bin/brew shellenv)"
log() { # print named params to stderr
  for name in $*; do
    echo ${(r:8:)name} ${(P)name} >>/dev/stderr
  done
}

# get input from stdin
input=$(cat); log input

# parse the input
parts=(${(s(:))input}) # split on :
prefix=$parts[1]
icon=$parts[2]
url="https://api.iconify.design/${prefix}.json?icons=${icon}"; log url

# get svg string (`brew install httpie`, `brew install jq`)
svg=$(http get $url | jq -r ".icons.\"$icon\".body")

# wrap in svg tag
svg="<svg xmlns=\"http://www.w3.org/2000/svg\">${svg}</svg>"

# save to file
svg_name="${prefix}-${icon}.svg"
out_file="${HOME}/Downloads/${svg_name}"; log out_file
echo -n $svg > $out_file

# return the file name
echo -n $svg_name

Script development tips

While developing a script, you can test it from the command line by setting any required variables in the call. For example:

zsh
POPCLIP_TEXT="my test text" POPCLIP_OPTION_FOO="foo" ./myscript

Or export them before calling the script:

zsh
export POPCLIP_TEXT="my test text"
export POPCLIP_OPTION_FOO="foo"
./myscript

When testing a script that uses the stdin field, you can pipe in a string from the command line:

zsh
echo "my test text" | ./myscript