diff --git a/Makefile b/Makefile index ea04302..bd0ebe5 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,20 @@ DATADIR ?= $(PREFIX)/share LIBDIR ?= $(DATADIR)/middleclick-autoscroll/lib LOCALEDIR ?= $(DATADIR)/locale MANDIR ?= $(DATADIR)/man + +# Where systemd looks for user units. For a normal install into /usr this is +# asked of systemd itself, because the answer is not the same everywhere - a +# distribution that still keeps /lib separate from /usr/lib says so here - and +# only guessed at when there is no systemd installed to ask. +# +# A build with a prefix of its own keeps the units under that prefix instead. +# systemd searches $(PREFIX)/lib/systemd/user as well, and a file outside the +# prefix it was asked for is not this build's to place. +ifeq ($(PREFIX),/usr) +USERUNITDIR ?= $(shell pkg-config --variable=systemduserunitdir systemd 2>/dev/null || echo /usr/lib/systemd/user) +else USERUNITDIR ?= $(PREFIX)/lib/systemd/user +endif LINGUAS := de MOFILES := $(patsubst %,po/%.mo,$(LINGUAS)) diff --git a/README.md b/README.md index aa7dbe3..75ed7d3 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,14 @@ underneath are identified by what they ship rather than by their name, and each of them is handled. ```bash -paru -S middleclick-autoscroll +paru -S middleclick-autoscroll # Arch and its derivatives middleclick-autoscroll enable ``` +There is no package for anything else yet, so elsewhere it is `make && sudo +make install` from a checkout and then the same one command. See +[Building from source](#building-from-source). + That is the whole setup. Nothing else has to be configured, and no file has to be edited. @@ -31,9 +35,10 @@ back on: Getting that argument into one application is a five-minute job. Getting it into all of them, in a way that survives the next package upgrade, is not: -- Some applications read a flag file, some don't. +- Some applications read a flag file, some don't - and which do depends on the + distribution as much as on the application. - Some ship their own copy of Electron, some use the system one. -- Flatpaks see none of the host's configuration. +- Flatpaks and snaps see none of the host's configuration. - An application that starts itself at login uses a different entry than the one in the menu, and Discord launched at login used to behave differently from Discord launched by hand. @@ -83,7 +88,7 @@ on — with the space bar: ``` **[4] Settings** has the categories — Electron and CEF applications, browsers, -Flatpaks, autostart entries, Steam, Spotify, whether to watch for new +Flatpaks, snaps, autostart entries, Steam, Spotify, whether to watch for new applications, and a field for extra Chromium arguments if you want any. There is a configuration file behind all of this. You are never asked to open @@ -95,8 +100,12 @@ Two routes, picked per application. | | | |---|---| -| **Flag file** | Arch's Electron and Chromium wrappers read extra arguments from `~/.config/-flags.conf`. This is the good one: it is the supported way to pass arguments, it survives package upgrades untouched, and it applies to a launch from a terminal as much as one from the menu. | -| **Desktop entry** | For applications that ship their own binary with no wrapper, and for Flatpaks, a copy of the entry with the argument appended goes into `~/.local/share/applications`, where it shadows the system one. | +| **Flag file** | Where the launcher reads extra arguments from `~/.config/-flags.conf`. This is the good one: it is the supported way to pass arguments, it survives package upgrades untouched, and it applies to a launch from a terminal as much as one from the menu. Arch's Electron and Chromium packages all work this way, and a number of individual vendors' launchers do everywhere else. | +| **Desktop entry** | For applications that ship their own binary with no wrapper, and for everything inside a Flatpak or a snap, a copy of the entry with the argument appended goes into `~/.local/share/applications`, where it shadows the system one. | + +Which of the two an application ends up on is decided by reading its launcher, +never by knowing which distribution this is. Nothing here has a list of +distributions in it any more than it has a list of applications. Entries that already live in `~/.local/share/applications` — AppImages, web app shortcuts — are edited in place and the original is kept. So are the entries in @@ -148,10 +157,15 @@ started once, at the start. ## Applications installed later A systemd user path unit watches every directory a launcher can appear in — -`/usr/share/applications`, the Flatpak exports, `~/.local/share/applications`, -`~/.config/autostart` — plus Steam's helper script. Anything new is handled -within a second of being installed, whether it came from pacman, the AUR, -Flatpak or an AppImage manager. There is no hook to install per package manager. +`/usr/share/applications`, the Flatpak exports, snapd's export directory, the +NixOS and Guix profiles, `~/.local/share/applications`, `~/.config/autostart` — +plus Steam's helper script. Anything new is handled within a second of being +installed, whether it came from pacman, apt, dnf, zypper, the AUR, Flatpak, +snapd or an AppImage manager. There is no hook to install per package manager, +which is the only reason one program can cover all of them. + +Without systemd nothing breaks; new applications are picked up the next time +`middleclick-autoscroll apply` runs instead of on their own. ## What it will not guess @@ -191,11 +205,34 @@ Run this before uninstalling the package. See `man middleclick-autoscroll` for the details. +## Distributions + +Any of them. Nothing here is keyed to a distribution name — what differs is +which of the two routes above an application ends up on, and that is read off +its launcher. + +On Arch and its derivatives most Electron and Chromium packages ship a wrapper +that reads a flag file, so most applications take that route. On Debian, +Ubuntu, Fedora and openSUSE the equivalent file lives under `/etc` and belongs +to the system rather than to you, so there is no flag file to write and those +applications go through their launcher entry instead. Both work. The flag file +is only the nicer of the two, because it applies to a launch from a terminal as +well. + +Snaps are handled the way Flatpaks are: what a snap ships lives in its own +mounted tree, `/snap/bin/` is a shim into snapd and says nothing about +what is behind it, and the launcher entry is the only way in. Each has a switch +of its own in the settings. + +Steam is found wherever the installation actually is — `~/.local/share/Steam` +for Valve's own package and Arch's, `~/.steam/debian-installation` for +Debian's, and inside the private tree for the Flatpak and the snap. + ## Requirements -Arch or an Arch derivative (CachyOS, EndeavourOS, Manjaro), bash, systemd for -the watcher. Nothing outside your home directory is ever written to, and running -it as root is refused. +Bash 4.2 or newer, GNU coreutils, and systemd for the watcher — that is all, +and it is what a desktop Linux install already has. Nothing outside your home +directory is ever written to, and running it as root is refused. ## Building from source @@ -204,6 +241,16 @@ make sudo make install ``` +`make` needs `msgfmt` (gettext) for the translations and `scdoc` for the man +page; both are optional and skipped with a note when missing. `make install` +puts the systemd user units where systemd itself says they go, and honours the +usual `PREFIX` and `DESTDIR`: + +```bash +make PREFIX=/usr/local +sudo make PREFIX=/usr/local install +``` + `make check` runs `bash -n` and, if installed, `shellcheck` over every script. ## License diff --git a/doc/middleclick-autoscroll.1.scd b/doc/middleclick-autoscroll.1.scd index 5c3f93d..de4af83 100644 --- a/doc/middleclick-autoscroll.1.scd +++ b/doc/middleclick-autoscroll.1.scd @@ -18,7 +18,9 @@ on the command line. *middleclick-autoscroll* finds every Chromium-based application on the system and puts that argument somewhere the application will actually read it, then -keeps doing so for anything installed later. +keeps doing so for anything installed later. It works on any distribution: +which of the routes below an application takes is read off its launcher, not +assumed from where the launcher came from. Run without a command it shows an interactive menu. Everything it can be told is reachable from there; the configuration file behind it does not need to be @@ -66,7 +68,7 @@ is refused. Two ways, chosen per application. *Flag file* - Arch's Electron and Chromium wrappers read extra arguments from + Where the launcher reads extra arguments from _$XDG_CONFIG_HOME/-flags.conf_. This is the preferred route: it is the supported way to pass arguments, it survives package upgrades, and it applies to a launch from a terminal as much as one from the menu. An @@ -74,12 +76,22 @@ Two ways, chosen per application. rather than duplicated - Chromium keeps only the last occurrence of that option, so a second one would switch the first one off. + Arch's Electron and Chromium packages all wrap their binaries this way, and + so do a number of individual vendors' launchers elsewhere. Whether a given + launcher does is read off the launcher itself, never assumed from the + distribution: only one that really names such a file takes this route. + *Desktop entry* For applications that ship their own binary with no wrapper, and for - Flatpaks, a copy of the desktop entry with the argument appended is written - to _~/.local/share/applications_, where it shadows the system one. Entries - that already live there - AppImages, web app shortcuts - are edited in - place, with the original kept. + everything inside a Flatpak or a snap, a copy of the desktop entry with the + argument appended is written to _~/.local/share/applications_, where it + shadows the system one. Entries that already live there - AppImages, web + app shortcuts - are edited in place, with the original kept. + + This is the route everything takes on the distributions whose Chromium + wrappers keep their equivalent file under _/etc_, where it is the system's + to write and not the user's: Debian, Ubuntu, Fedora and openSUSE among + them. A generated entry is marked *X-MCA-Generated* and an entry edited in place *X-MCA-Patched*. The two are never confused: the first is deleted when @@ -98,6 +110,11 @@ helper, inside Steam's own installation: ~/.local/share/Steam/ubuntu12_64/steamwebhelper_sniper_wrap.sh +Where that installation is depends on how Steam was installed: +_~/.local/share/Steam_ for Valve's own package and Arch's, +_~/.steam/debian-installation_ for Debian's, and the private tree of the +sandbox for the Flatpak and the snap. All of them are looked at. + Steam compares the installed files against its manifest at every start - by size, not by content - and restores whatever differs, so its launcher entry gets *-noverifyfiles*. So does its entry in _~/.config/autostart_, which Steam @@ -125,8 +142,8 @@ anyway. The official client is CEF rather than Electron. Installed through *spotify-launcher*, it is started by a program that builds its own command line and has a configuration file with a slot for extra arguments; that slot is -where the flag goes. Installed as a plain package, it is an ordinary desktop -entry and needs nothing special. +where the flag goes. Installed as a plain package, a Flatpak or a snap, it is +an ordinary desktop entry and needs nothing special. # WHAT CANNOT BE DETECTED @@ -155,9 +172,15 @@ _~/.cache/middleclick-autoscroll/detect_ *NO_COLOR* Disables colour. +# REQUIREMENTS + +Bash 4.2 or newer and GNU coreutils. The watcher needs a systemd user session; +without one everything else works and applications installed later are picked +up at the next *apply* rather than on their own. + # SEE ALSO -*systemctl*(1), *flatpak*(1) +*systemctl*(1), *flatpak*(1), *snap*(8) # AUTHORS diff --git a/po/de.po b/po/de.po index c8edeb1..4bc0922 100644 --- a/po/de.po +++ b/po/de.po @@ -106,6 +106,9 @@ msgstr "Chromium-basierte Browser" msgid "Flatpak applications" msgstr "Flatpak-Anwendungen" +msgid "Snap applications" +msgstr "Snap-Anwendungen" + msgid "Programs that start themselves at login" msgstr "Programme, die sich beim Anmelden selbst starten" diff --git a/po/middleclick-autoscroll.pot b/po/middleclick-autoscroll.pot index 48ec7d2..45cd735 100644 --- a/po/middleclick-autoscroll.pot +++ b/po/middleclick-autoscroll.pot @@ -106,6 +106,9 @@ msgstr "" msgid "Flatpak applications" msgstr "" +msgid "Snap applications" +msgstr "" + msgid "Programs that start themselves at login" msgstr "" diff --git a/res/systemd/middleclick-autoscroll.path b/res/systemd/middleclick-autoscroll.path index d23f833..d86984a 100644 --- a/res/systemd/middleclick-autoscroll.path +++ b/res/systemd/middleclick-autoscroll.path @@ -3,21 +3,33 @@ Description=Watch for newly installed Chromium-based applications Documentation=man:middleclick-autoscroll(1) [Path] -# Every way an application can turn up: a package, a Flatpak, something +# Every way an application can turn up: a package, a Flatpak, a snap, something # installed by hand. Watching where the launchers land means there is no hook -# to write per package manager - pacman, flatpak and an AppImage manager all -# end up here. +# to write per package manager - pacman, apt, dnf, zypper, flatpak, snapd and +# an AppImage manager all end up here. +# +# A directory that does not exist on this system costs nothing: systemd watches +# the nearest parent that does and picks the path up when it appears. PathModified=/usr/share/applications PathModified=/usr/local/share/applications PathModified=%h/.local/share/applications PathModified=%h/.config/autostart PathModified=/var/lib/flatpak/exports/share/applications PathModified=%h/.local/share/flatpak/exports/share/applications +PathModified=/var/lib/snapd/desktop/applications + +# NixOS and Guix build the search path out of profiles rather than /usr. +PathModified=/run/current-system/sw/share/applications +PathModified=%h/.nix-profile/share/applications +PathModified=%h/.guix-profile/share/applications # Steam replaces the script that starts its interface on every client update, -# which takes the patch with it. +# which takes the patch with it. Where that installation lives depends on how +# Steam was installed. PathChanged=%h/.local/share/Steam/ubuntu12_64/steamwebhelper_sniper_wrap.sh +PathChanged=%h/.steam/debian-installation/ubuntu12_64/steamwebhelper_sniper_wrap.sh PathChanged=%h/.var/app/com.valvesoftware.Steam/.local/share/Steam/ubuntu12_64/steamwebhelper_sniper_wrap.sh +PathChanged=%h/snap/steam/common/.local/share/Steam/ubuntu12_64/steamwebhelper_sniper_wrap.sh Unit=middleclick-autoscroll.service diff --git a/src/lib/apply.sh b/src/lib/apply.sh index f2d06d1..dc09693 100644 --- a/src/lib/apply.sh +++ b/src/lib/apply.sh @@ -10,7 +10,7 @@ MCA_CHANGES=0 MCA_ROUTES=() -# mca_route +# mca_route [packaging] # What happens to one application, left in MCA_ROUTE: # flags - the launcher reads a flag file; write it there # desktop - no flag file; shadow or edit the desktop entry @@ -24,7 +24,7 @@ MCA_ROUTES=() MCA_ROUTE='' mca_route() { - local kind="$1" id="$2" prog="$3" + local kind="$1" id="$2" prog="$3" packaging="${4:-native}" if [[ $kind == steam ]]; then if mca_config_list_has Skip "$id" || [[ $CFG_STEAM != yes ]]; then @@ -35,14 +35,14 @@ mca_route() { return fi - if ! mca_kind_wanted "$kind" "$id"; then + if ! mca_kind_wanted "$kind" "$id" "$packaging"; then [[ $kind == unknown ]] && MCA_ROUTE=unknown || MCA_ROUTE=off return fi - # A Flatpak has its own copy of everything and none of the host's wrappers, - # so the desktop entry is the only way in. - if [[ $kind == flatpak ]]; then + # A Flatpak or a snap carries its own copy of everything and sees none of + # the host's wrappers, so the desktop entry is the only way in. + if [[ $packaging != native ]]; then MCA_ROUTE=desktop return fi @@ -59,7 +59,7 @@ mca_route() { # as it likes to be - it writes only what differs, which is what keeps the # watcher from chasing its own changes. mca_apply() { - local i id file prog kind route steam_done=0 + local i id file prog kind packaging route steam_done=0 MCA_CHANGES=0 MCA_ROUTES=() @@ -73,8 +73,9 @@ mca_apply() { file="${MCA_FILES[i]}" prog="${MCA_PROGS[i]}" kind="${MCA_KINDS[i]}" + packaging="${MCA_PACKAGING[i]}" - mca_route "$kind" "$id" "$prog" + mca_route "$kind" "$id" "$prog" "$packaging" route="$MCA_ROUTE" MCA_ROUTES[i]="$route" @@ -153,9 +154,9 @@ mca_revert() { # The watcher # --------------------------------------------------------------------------- # A systemd user path unit watching every directory a desktop entry can appear -# in. That covers a package installed with pacman, a Flatpak, an AppImage -# registered by hand and a Steam client update, without a hook per package -# manager. +# in. That covers a package from whatever the distribution's package manager +# is, a Flatpak, a snap, an AppImage registered by hand and a Steam client +# update, without a hook per package manager. MCA_UNIT_PATH="middleclick-autoscroll.path" MCA_UNIT_SERVICE="middleclick-autoscroll.service" @@ -201,7 +202,8 @@ mca_count_routes() { MCA_ROUTES=() for i in "${!MCA_IDS[@]}"; do - mca_route "${MCA_KINDS[i]}" "${MCA_IDS[i]}" "${MCA_PROGS[i]}" + mca_route "${MCA_KINDS[i]}" "${MCA_IDS[i]}" "${MCA_PROGS[i]}" \ + "${MCA_PACKAGING[i]}" route="$MCA_ROUTE" MCA_ROUTES[i]="$route" case "$route" in diff --git a/src/lib/common.sh b/src/lib/common.sh index e833c8f..9c2682f 100644 --- a/src/lib/common.sh +++ b/src/lib/common.sh @@ -34,7 +34,7 @@ MCA_CACHEDIR="${MCA_CACHEDIR:-${MCA_XDG_CACHE}/${MCA_NAME}}" # Where generated desktop entries go. A file here shadows the one with the same # name in /usr/share/applications, which is how an application gets extra -# command line arguments without touching anything pacman owns. +# command line arguments without touching a file the package manager owns. MCA_APPDIR="${MCA_XDG_DATA}/applications" # Copies of every file that is edited in place rather than shadowed. @@ -60,8 +60,15 @@ mca_ui_locale() { [[ -z $l ]] && l="${LANG:-}" fi - if [[ -z $l && -r /etc/locale.conf ]]; then - l="$(sed -n 's/^LANG=//p' /etc/locale.conf | tr -d '"' | head -n1)" + # systemd writes /etc/locale.conf and most distributions use it; Debian and + # Ubuntu keep the same LANG= line in /etc/default/locale instead. + if [[ -z $l ]]; then + local f + for f in /etc/locale.conf /etc/default/locale; do + [[ -r $f ]] || continue + l="$(sed -n 's/^LANG=//p' "$f" | tr -d '"' | head -n1)" + [[ -n $l ]] && break + done fi printf '%s\n' "${l:-C}" diff --git a/src/lib/config.sh b/src/lib/config.sh index 19e2182..a67d41d 100644 --- a/src/lib/config.sh +++ b/src/lib/config.sh @@ -139,6 +139,7 @@ mca_config_load() { CFG_APPS=no; mca_config_bool PatchApps yes && CFG_APPS=yes CFG_BROWSERS=no; mca_config_bool PatchBrowsers yes && CFG_BROWSERS=yes CFG_FLATPAK=no; mca_config_bool PatchFlatpak yes && CFG_FLATPAK=yes + CFG_SNAP=no; mca_config_bool PatchSnap yes && CFG_SNAP=yes CFG_AUTOSTART=no; mca_config_bool PatchAutostart yes && CFG_AUTOSTART=yes CFG_STEAM=no; mca_config_bool PatchSteam yes && CFG_STEAM=yes CFG_SPOTIFY=no; mca_config_bool PatchSpotify yes && CFG_SPOTIFY=yes diff --git a/src/lib/detect.sh b/src/lib/detect.sh index db8ab52..8adebd5 100644 --- a/src/lib/detect.sh +++ b/src/lib/detect.sh @@ -27,9 +27,23 @@ MCA_MARKERS=( MCA_SYSTEM_DIRS=( / /bin /lib /lib32 /lib64 /sbin /usr /usr/bin /usr/lib /usr/lib32 /usr/lib64 /usr/libexec /usr/sbin /usr/local /usr/local/bin - /usr/local/lib /opt + /usr/local/lib /usr/local/libexec /opt ) +# The same thing for the layouts that put a machine triplet in the path. +# Debian and Ubuntu keep the shared libraries in /usr/lib/x86_64-linux-gnu +# rather than /usr/lib, so that directory is every bit as shared as /usr/lib is +# elsewhere and a marker sitting in it belongs to nobody in particular. +MCA_SYSTEM_DIR_GLOBS=( + '/usr/lib/*-linux-gnu*' '/usr/lib32/*-linux-gnu*' + '/usr/lib64/*-linux-gnu*' '/usr/local/lib/*-linux-gnu*' +) + +# Where snapd mounts the installed snaps. /snap is the usual place and the one +# the shims point into; distributions that keep /snap free of a top-level +# directory use the second. +MCA_SNAP_DIRS=(/snap /var/lib/snapd/snap) + # Strings in a launcher script that mean it starts a Chromium or Electron # process, for the wrappers whose command line is assembled out of variables and # cannot be followed from the outside. @@ -38,23 +52,51 @@ MCA_SYSTEM_DIRS=( # "chromium" on its own is not one of them: /usr/bin/xdg-open lists every # browser it knows how to start, and that is not a browser. # -# The flag file convention is an Arch packaging habit and says nothing about the -# engine either, so it is not in here. -MCA_SCRIPT_HINTS='ELECTRON_|app\.asar|chrome-sandbox|libcef|enable-blink-features|ozone-platform-hint' +# The flag file convention is one distribution's packaging habit and says +# nothing about the engine either, so it is not in here. +# +# CHROMIUM_FLAGS and CHROME_WRAPPER earn their place: they are the variables +# the Debian, Fedora and openSUSE Chromium wrappers and Google's own Chrome +# wrapper build their command line out of, and nothing else sets them. +MCA_SCRIPT_HINTS='ELECTRON_|app\.asar|chrome-sandbox|libcef|enable-blink-features|ozone-platform-hint|CHROMIUM_FLAGS|CHROME_WRAPPER|CHROME_VERSION_EXTRA' # --------------------------------------------------------------------------- # Desktop entries # --------------------------------------------------------------------------- +# Where Flatpak and snapd put the launchers they export. Both add these to +# XDG_DATA_DIRS themselves, through a file in /etc/profile.d - but only for a +# session that was started after they were installed, and only for a session +# manager that reads it at all. They are appended, after everything XDG names, +# so a directory that is already in the search path keeps its own position and +# the ones that were missing are still scanned. +mca_extra_desktop_dirs() { + printf '%s\n' \ + "$MCA_XDG_DATA/flatpak/exports/share/applications" \ + /var/lib/flatpak/exports/share/applications \ + /var/lib/snapd/desktop/applications +} + # The directories a desktop entry can come from, most specific first - which is # also XDG lookup order, so the first file found for an id is the one that is # actually used. mca_desktop_dirs() { local dirs="${XDG_DATA_DIRS:-/usr/local/share:/usr/share}" d - printf '%s\n' "$MCA_APPDIR" - while IFS= read -r -d: d; do - [[ -n $d ]] && printf '%s/applications\n' "${d%/}" - done <<< "${dirs}:" + local -A seen=() + + while IFS= read -r d; do + [[ -n $d ]] || continue + d="${d%/}" + [[ -n ${seen[$d]+set} ]] && continue + seen[$d]=1 + printf '%s\n' "$d" + done < <( + printf '%s\n' "$MCA_APPDIR" + while IFS= read -r -d: d; do + [[ -n $d ]] && printf '%s/applications\n' "${d%/}" + done <<< "${dirs}:" + mca_extra_desktop_dirs + ) } # mca_desktop_get @@ -192,7 +234,7 @@ mca_exec_is_steam_link() { case "${prog##*/}" in steam|steam-runtime) return 0 ;; esac - [[ $prog == flatpak:com.valvesoftware.Steam ]] + [[ $prog == flatpak:com.valvesoftware.Steam || $prog == snap:steam ]] } # --------------------------------------------------------------------------- @@ -265,6 +307,11 @@ _mca_has_markers() { for s in "${MCA_SYSTEM_DIRS[@]}"; do [[ $dir == "$s" ]] && return 1 done + for s in "${MCA_SYSTEM_DIR_GLOBS[@]}"; do + # Unquoted on purpose - these are patterns, not names. + # shellcheck disable=SC2053 + [[ $dir == $s ]] && return 1 + done for m in "${MCA_MARKERS[@]}"; do [[ -e "$dir/$m" ]] && return 0 @@ -273,23 +320,118 @@ _mca_has_markers() { return 1 } +# The plain assignments the script made before it handed over, for +# _mca_script_subst to read. A variable of its own rather than something passed +# around: every caller of _mca_script_target reads it through a command +# substitution, so each call already works on a copy and there is nothing here +# that two of them could collide over. +declare -A MCA_SCRIPT_VARS=() + +# _mca_script_subst +# The text with $NAME and ${NAME} replaced by what the script assigned to them, +# left in MCA_SUBST. +# +# Fails as soon as something turns up that only a running shell could work out +# - a positional parameter, a name the script never set, a default value. That +# is the point: an unresolvable path has to come out as no path at all, never +# as a wrong one. +MCA_SUBST='' + +_mca_script_subst() { + local text="$1" out='' rest name + + while [[ $text == *'$'* ]]; do + out+="${text%%\$*}" + rest="${text#*\$}" + + if [[ $rest == '{'* ]]; then + [[ $rest == *'}'* ]] || return 1 + name="${rest%%\}*}"; name="${name#\{}" + rest="${rest#*\}}" + else + name="${rest%%[!A-Za-z0-9_]*}" + rest="${rest:${#name}}" + fi + + [[ $name =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || return 1 + [[ -n ${MCA_SCRIPT_VARS[$name]+set} ]] || return 1 + + out+="${MCA_SCRIPT_VARS[$name]}" + text="$rest" + done + + MCA_SUBST="$out$text" + return 0 +} + # _mca_script_target