Last active
March 25, 2026 23:03
-
-
Save victorlin/6955cef5075560bffeb156d7e1d2d37f to your computer and use it in GitHub Desktop.
helper script to copy extension files from one Chromium profile to another
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # one column for `select` | |
| COLUMNS=1 | |
| main() { | |
| local source_profile | |
| local ext_id | |
| local dest | |
| local profiles=() | |
| local other_profiles=() | |
| mapfile -t profiles < <(find_profiles) | |
| source_profile=$(choose_source_profile "${profiles[@]}") | |
| echo "" >&2 | |
| ext_id=$(choose_extension "$source_profile/Extensions") | |
| mapfile -t other_profiles < <(find_other_installations "$source_profile" "$ext_id" "${profiles[@]}") | |
| echo "" | |
| if [ ${#other_profiles[@]} -eq 0 ]; then | |
| echo "No other profiles have this extension installed." | |
| return 0 | |
| fi | |
| dest=$(choose_dest "${other_profiles[@]}") | |
| echo "" | |
| generate_copy_commands "$source_profile" "$ext_id" "$dest" | |
| } | |
| find_profiles() { | |
| local profiles=() | |
| echo "Finding Chromium profiles…" >&2 | |
| echo "" >&2 | |
| mapfile -t profiles < <(find "$HOME/Library/Application Support/" -type d -name "Extensions" 2>/dev/null | while read -r ext_dir; do dirname "$ext_dir"; done) | |
| if [ ${#profiles[@]} -eq 0 ]; then | |
| echo "No Chromium profiles found." >&2 | |
| return 1 | |
| fi | |
| printf '%s\n' "${profiles[@]}" | |
| } | |
| choose_source_profile() { | |
| local profiles=("$@") | |
| local profile_display | |
| local display_dirs=() | |
| for profile in "${profiles[@]}"; do | |
| display_dirs+=("${profile#"$HOME/Library/Application Support/"}") | |
| done | |
| echo "Choose a profile to copy extension files from:" >&2 | |
| select profile_display in "${display_dirs[@]}"; do | |
| if [ -n "$profile_display" ]; then | |
| echo "You selected: $profile_display" >&2 | |
| printf '%s\n' "$HOME/Library/Application Support/$profile_display" | |
| return 0 | |
| else | |
| echo "Invalid selection." >&2 | |
| fi | |
| done | |
| } | |
| choose_extension() { | |
| local ext_dir="$1" | |
| local ext_records=() | |
| local ext_choice | |
| echo "Loading extensions…" >&2 | |
| # list the extensions by ID and name (from manifest.json) | |
| for id_dir in "$ext_dir"/*; do | |
| if [ -d "$id_dir" ]; then | |
| local ext_id | |
| ext_id=$(basename "$id_dir") | |
| # Extensions usually have a version subdirectory, such as 1.0.0_0/manifest.json | |
| local manifest_file | |
| manifest_file=$(find "$id_dir" -maxdepth 2 -name "manifest.json" | head -n 1) | |
| if [ -n "$manifest_file" ] && [ -f "$manifest_file" ]; then | |
| # Use jq to grab the best readable name without localized placeholders | |
| local ext_name | |
| ext_name=$(jq -r '[ .browser_action.default_title, .action.default_title, .name, .homepage_url, "Unknown Name" ] | map(select(type == "string" and (startswith("__MSG_") | not))) | .[0]' "$manifest_file" 2>/dev/null) | |
| if [ -z "$ext_name" ] || [ "$ext_name" = "null" ]; then | |
| ext_name="Unknown (unable to parse extension manifest)" | |
| fi | |
| ext_records+=("$ext_name"$'\t'"$ext_id") | |
| fi | |
| fi | |
| done | |
| # Sort extensions alphabetically by name (case-insensitive) | |
| mapfile -t ext_records < <(printf '%s\n' "${ext_records[@]}" | sort -f) | |
| local ext_names=() | |
| local ext_ids=() | |
| local ext_choices=() | |
| for item in "${ext_records[@]}"; do | |
| local ext_name="${item%$'\t'*}" | |
| local ext_id="${item#*$'\t'}" | |
| ext_names+=("$ext_name") | |
| ext_ids+=("$ext_id") | |
| ext_choices+=("$ext_name <https://chromewebstore.google.com/detail/$ext_id>") | |
| done | |
| echo "" >&2 | |
| echo "Choose an extension to copy files from:" >&2 | |
| select ext_choice in "${ext_choices[@]}"; do | |
| if [ -n "$ext_choice" ]; then | |
| for i in "${!ext_choices[@]}"; do | |
| if [[ "${ext_choices[$i]}" = "$ext_choice" ]]; then | |
| echo "You selected: ${ext_names[$i]}" >&2 | |
| printf '%s\n' "${ext_ids[$i]}" | |
| return 0 | |
| fi | |
| done | |
| else | |
| echo "Invalid selection." >&2 | |
| fi | |
| done | |
| } | |
| find_other_installations() { | |
| local source_profile="$1" | |
| local ext_id="$2" | |
| shift 2 | |
| local profiles=("$@") | |
| local other_profiles=() | |
| for profile in "${profiles[@]}"; do | |
| local other_ext_dir="$profile/Extensions" | |
| if [ "$profile" != "$source_profile" ] && [ -d "$other_ext_dir/$ext_id" ]; then | |
| other_profiles+=("$profile") | |
| fi | |
| done | |
| printf '%s\n' "${other_profiles[@]}" | |
| } | |
| choose_dest() { | |
| local dest_profiles=("$@") | |
| local display_profiles=() | |
| for profile in "${dest_profiles[@]}"; do | |
| display_profiles+=("${profile#"$HOME/Library/Application Support/"}") | |
| done | |
| echo "The extension is also installed in the following profiles." >&2 | |
| echo "" >&2 | |
| echo "Choose one to copy the extension files to:" >&2 | |
| select dest_display in "${display_profiles[@]}"; do | |
| if [ -n "$dest_display" ]; then | |
| local dest="" | |
| for i in "${!display_profiles[@]}"; do | |
| if [[ "${display_profiles[$i]}" = "$dest_display" ]]; then | |
| dest="${dest_profiles[$i]}" | |
| echo "You selected destination: $dest_display" >&2 | |
| printf '%s\n' "$dest" | |
| return 0 | |
| fi | |
| done | |
| else | |
| echo "Invalid selection." >&2 | |
| fi | |
| done | |
| } | |
| generate_copy_commands() { | |
| local source_profile="$1" | |
| local ext_id="$2" | |
| local dest="$3" | |
| local source_ext_dir="$source_profile/Extensions" | |
| echo "Run the following commands to copy the extension:" | |
| echo "" | |
| print_copy_command "$source_ext_dir/$ext_id" "$dest/Extensions/" | |
| print_copy_command "$source_profile/Local Extension Settings/$ext_id" "$dest/Local Extension Settings/" | |
| print_copy_command "$source_profile/IndexedDB/chrome-extension_${ext_id}_0.indexeddb.leveldb" "$dest/IndexedDB/" | |
| print_copy_command "$source_profile/IndexedDB/chrome-extension_${ext_id}_0.indexeddb.blob" "$dest/IndexedDB/" | |
| print_copy_command "$source_profile/Sync Extension Settings/$ext_id" "$dest/Sync Extension Settings/" | |
| print_copy_command "$source_profile/Local App Settings/$ext_id" "$dest/Local App Settings/" | |
| } | |
| print_copy_command() { | |
| local source_path="$1" | |
| local dest_dir="$2" | |
| if [ -d "$source_path" ]; then | |
| echo "cp -R \"$source_path\" \"$dest_dir\"" | |
| elif [ -f "$source_path" ]; then | |
| echo "cp \"$source_path\" \"$dest_dir\"" | |
| fi | |
| } | |
| main "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment