3

VSCode's portable mode doesn't support auto-updates, unlike its normal installer-based versions. I happen to use MSYS2's UCRT64 environment which makes the Linux tools I like play nice with the Windows environment I need, so I wrote a script to do the updating for me.

I prefer zsh, but shellcheck doesn't support zsh or the #!/bin/env $SHELL construct, so I had to use #!/bin/bash to get it to check my script. It also choked on one of my comments which started with # shellcheck, which it interpreted as meaning something, so I had to remove that. Nevertheless, here is the script as I wrote it originally (not getting shellcheck to run on it):

#!/bin/env bash

DEBUG=0
if [[ "$1" == "--debug" ]]; then
    DEBUG=1
fi

instdir="/c/Users/User/Applications/vscode"

# code --version doesn't like sed, so we do it the janky way
lver=$(code --version | head -n 2 | tail -n 1)
lnam=$(code --version | head -n 1)

echo "Checking for updates..."

checkdir="$HOME/tmp-vsc-upd"
checkres="$checkdir/tmp-vsc-checkres"

mkdir -p "$checkdir"

curl -s "https://update.code.visualstudio.com/api/update/win32-x64-archive/stable/$lver" -o "$checkres"

if [[ $(wc -l "$checkres") = 0 ]]; then
    [[ "$DEBUG" -ne 0 ]] && rm -r "$checkdir"
    echo "No updates found."
    exit 0
fi

uver=$(jq .version "$checkres" | cut -c 2- | rev | cut -c 3- | rev)
unam=$(jq .name "$checkres" | cut -c 2- | rev | cut -c 3- | rev)

echo "Local version: $lnam ($lver)"
echo "Upstream version: $unam ($uver)"

# shellcheck suggests pgrep, but MSYS2's pgrep can't see Windows processes
if [[ $(ps -eW | grep -c "Code.exe") -gt 0 ]]; then
    [[ "$DEBUG" -ne 0 ]] && rm -r "$checkdir"
    echo "Close VSCode before updating."
    exit 0
fi

read -r -n 1 -p "Replace local version with upstream version? (y/N) " action
if [[ -z "$action" ]]; then
    action="n"
else
    echo ""
fi

case "$action" in
    y|Y )
        echo "Downloading new version..."
        src=$(jq .url "$checkres" | cut -c 2- | rev | cut -c 3- | rev)
        out="$checkdir/VSCode-win32-x64-$unam.zip"
        curl -o "$out" "$src"

        echo "Checking file integrity..."
        upsum=$(jq .sha256hash "$checkres" | cut -c 2- | rev | cut -c 3- | rev)
        acsum=$(sha256sum "$out" | cut -d ' ' -f 1)
        if [[ "$upsum" == "$acsum" ]]; then
            echo "File OK."
        else
            [[ "$DEBUG" -ne 0 ]] && rm -r "$checkdir"

            echo "File may be corrupted:"
            echo "    Expected: $upsum"
            echo "    Received: $acsum"
            echo "Aborting."

            exit 0
        fi

        echo "Extracting files..."
        unzip -u -o -qq "$out" -d "$instdir"
        echo "Finished extracting."

        echo ""

        [[ "$DEBUG" -ne 0 ]] && rm -r "$checkdir"
        echo "Finished updating."

        exit 0
    ;;

    * )
        [[ "$DEBUG" -ne 0 ]] && rm -r "$checkdir"
        echo "Aborting."
        exit 0
    ;;
esac

I would have to download multiple versions of VSCode to properly test this out, so I didn't do that. But it did automate my 6 minute task in 6 hours once.

shellcheck is happy with my little script, but there are probably more improvements to be made that it can't see / wasn't designed to see.

1 Answer 1

3

It's a pretty nice script, I like it. Special thanks for checking the script with shellcheck!

Check for requirements early

The program depends on some non-standard tools such as jq. It's good to check early in the script if the required tools actually exist and fail early with a clear message, rather letting the program crash later due to the missing tool, and without a helpful message.

Check if a file is empty using [[ -s path/to/file ]]

I don't have MSYS at hand to verify, but this should not work:

if [[ $(wc -l "$checkres") = 0 ]]; then

The issue is that when invoked as wc -l path/to/file, the output is formatted as " <path/to/file>", so the condition will never be true.

To get just the count from wc without the path, you could invoke it as wc -l < path/to/file (notice the input redirection):

if [[ $(wc -l < "$checkres") = 0 ]]; then

But since you don't actually need to count lines, you just want to know if the file is empty or not, you could use the test -s builtin:

if ! [[ -s "$checkres" ]]; then

Use the exit code of grep to check if match was found

Instead of Command Substitution with $(...), counting lines and a numeric comparison like this:

if [[ $(ps -eW | grep -c "Code.exe") -gt 0 ]]; then

It's better to use the exit code of grep to check if a match was found or not:

if ps -eW | grep -q "Code.exe"; then

Note that this is especially useful in combination with the -q flag of grep, which makes it stop searching in the input as soon as it found a match.

An aside, in many systems this command would also match the grep command itself. A common workaround is to make the expression to match a pattern:

if ps -eW | grep -q "[C]ode.exe"; then

Get raw values from jq output using -r

This is hacky and inefficient way to strip the enclosing double-quotes from values in the output of jq, using multiple processes:

uver=$(jq .version "$checkres" | cut -c 2- | rev | cut -c 3- | rev)

A much simpler solution is possible thanks to the -r or --raw-output flag:

uver=$(jq -r .version "$checkres")

Use better variable names

I find the script hard to read because the variable names are a bit cryptic. I recommend to rename them so they describe better their purpose, for example:

  • lnam -> current_version_name
  • lver -> current_version_hash
  • unam, uver -> like lnam, lver
  • upsum -> expected_checksum
  • acsum -> actual_checksum
  • ... really, all the variables...

A simpler way to read the first two lines from a command

Instead of:

lver=$(code --version | head -n 2 | tail -n 1)
lnam=$(code --version | head -n 1)

You could use read:

{ read -r lnam; read -r lver; } < <(code --version)

Alternatively, using an array:

version_info=($(code --version))
lnam=${version_info[0]}
lver=${version_info[1]}

Exit with non-zero code in case of failures

The script does exit 0 at multiple places when it detects that something is off. The recommended practice is to exit with a non-zero code to signal that the execution was a failure. Using exit 1 is fine solution.

Use set -euo pipefail to fail fast on errors

A common mistake in scripts is forgetting to check that some important command runs successfully, and by default Bash will continue running the rest of the script, which may cause severe harm.

A good simple safeguard is to include set -euo pipefail at the top of the script, which will make Bash abort on the first failure, and also treat referencing unassigned variables as an error, among other things. For more details see help set and man bash.

Minor nits

echo "" is really the same as echo, no need for the empty string parameter.

In a command like curl -s "long url" -o "short value" I prefer to rearrange the terms as curl -s -o "short value" "long url" to keep the most important part of the command visible without horizontal scrolling. If anything important goes at the far right end of a line, there is a higher risk of overlooking it.

instdir, checkdir, and checkres are constants with static values, therefore it's good to group them together at the top of the script.

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.