Skip to content

copyclip: Copy Jupyter Notebook Cells to macOS Clipboard

The copyclip tool is a Bash script designed to extract and format the contents of a Jupyter Notebook (.ipynb) and copy it to the macOS clipboard using pbcopy. It parses notebook cells using Python and converts them to readable Markdown format, which is especially useful for sharing notebook content or documenting it outside of the Jupyter environment.


🛠️ Features

  • Parses .ipynb files using Python 3
  • Outputs Markdown-formatted content
  • Differentiates between code and markdown cells
  • Automatically copies the output to clipboard on macOS
  • Provides clear cell separation and visual structure

📦 Dependencies

  • python3 (installed by default on macOS)
  • pbcopy (macOS utility for clipboard access)
  • A valid .ipynb file

📄 Script Source

copyclip
#!/bin/bash

# Print usage function
usage() {
    echo "Usage: ./copyclip.sh [-h] [--no-output] <path_to_notebook.ipynb>"
    echo "Options:"
    echo "  -h, --help    Show this help message and exit"
    echo "  --no-output   Exclude cell outputs from the copied text"
}

# Initialize variables (output is included by default)
WITH_OUTPUT=1
FILE=""

# Parse arguments
while [[ "$#" -gt 0 ]]; do
    case $1 in
        -h|--help)
            usage
            exit 0
            ;;
        --no-output) 
            WITH_OUTPUT=0
            shift 
            ;;
        *) 
            if [ -z "$FILE" ]; then
                FILE="$1"
            fi
            shift 
            ;;
    esac
done

# Check if a file was provided
if [ -z "$FILE" ]; then
    usage
    exit 1
fi

# Check if the file exists
if [ ! -f "$FILE" ]; then
    echo "Error: File '$FILE' not found."
    exit 1
fi

# Use a quoted HEREDOC (<< 'EOF') so Bash ignores the backticks completely
python3 - "$FILE" "$WITH_OUTPUT" << 'EOF' | pbcopy
import sys, json

file_path = sys.argv[1]
with_output = sys.argv[2] == '1'

try:
    with open(file_path, 'r', encoding='utf-8') as f:
        nb = json.load(f)

    output = []

    for i, cell in enumerate(nb.get('cells', [])):
        cell_type = cell.get('cell_type', '')
        source = ''.join(cell.get('source', []))

        # Add a visual separator for clarity
        output.append(f'--- CELL {i+1} ({cell_type.upper()}) ---')

        if cell_type == 'code':
            # Wrap code in markdown code blocks
            output.append('```python')
            output.append(source)
            output.append('```')

            # Extract output if the flag is enabled (now the default)
            if with_output and 'outputs' in cell and cell['outputs']:
                output.append('\n**Output:**')
                output.append('```text')

                for out in cell['outputs']:
                    out_type = out.get('output_type', '')

                    if out_type == 'stream':
                        output.append(''.join(out.get('text', [])))
                    elif out_type in ('execute_result', 'display_data'):
                        data = out.get('data', {})
                        if 'text/plain' in data:
                            output.append(''.join(data['text/plain']))
                    elif out_type == 'error':
                        # Join the traceback for errors
                        output.append(''.join(out.get('traceback', [])))

                output.append('```')

        else:
            # Markdown cells are added as-is
            output.append(source)

        output.append('\n')

    # Print to stdout so bash can pipe it
    print('\n'.join(output))

except Exception as e:
    sys.stderr.write(f'Error processing notebook: {e}\n')
    sys.exit(1)
EOF

# Check exit status of the python script in the pipeline
if [ ${PIPESTATUS[0]} -eq 0 ]; then
    if [ "$WITH_OUTPUT" -eq 1 ]; then
        echo "✅ Successfully extracted '$FILE' (with outputs) and copied to clipboard."
    else
        echo "✅ Successfully extracted '$FILE' (without outputs) and copied to clipboard."
    fi
else
    echo "❌ Failed to copy to clipboard."
fi