# 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. **Tip: 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](https://www.popclip.app/dev/external-scripts.md) for the full story. **Warning: Submitting to the directory** Extensions submitted to the [Extensions Directory](https://www.popclip.app/extensions/) should use JavaScript actions in preference to Shell Script actions. A submission with a Shell Script action must include a [`shellScriptRationale`](https://www.popclip.app/extensions/submit.md#shell-script-policy) 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: | Key | Type | Description | | ----------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `shellScript` | String | A string to be run as a shell script. The string will be passed via standard input to the specified `interpreter`, invoked without arguments. | | `shellScriptFile` | String | The name of a file in the extension's package directory. See [Shell script file execution](#shell-script-file-execution) for more details. | | `interpreter` | String (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`. | | `stdin` | String (optional) | For script specified as `shellScriptFile` only. Set the name of a [script variable](https://www.popclip.app/dev/script-variables.md) to pass via standard input (stdin). If omitted, no standard input is provided to the script. | | `shellMode` | String (optional) | How the script is executed: `login` (the default), `nonlogin` or `none`. See [Shell mode](#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](https://www.popclip.app/dev/script-variables.md). 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](https://github.com/pilotmoon/PopClip-Extensions/tree/master/source/Say.popclipext) extension demonstrates a packaged shell script extension. ### Snippet examples **Note: About these examples** The examples are given as [code snippets](https://www.popclip.app/dev/snippets.md#inverted-syntax). Examples of passing the selected text to the `say` command to be spoken aloud: **Using shell variable** ```zsh #!/bin/zsh # #popclip # name: Say (variable) say $POPCLIP_TEXT ``` **Using stdin** ```zsh #!/bin/zsh # #popclip # name: Say (stdin) # stdin: text say ``` **With option** ```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}" # 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 ```