Home > Software design >  Porting concurrent Bash code to Powershell
Porting concurrent Bash code to Powershell

Time:04-16

I have the function below for writing storage (sync on Unix):

# Sync Progress Function
function syncStorage {

  printq "Writing storage, may take more than 5 minutes."
  printq "Although it seems slow, consider this process like flashing an ISO to a USB Drive."
  printq "Below is an innacurate indicator of mB left to write. It may decrease hundreds of megabytes in seconds."

  # shellcheck disable=SC2016
  sync & {
    # If the unsynced data (in kB) is greater than 50MB, then show the sync progress
    while [[ $(grep -e Dirty: /proc/meminfo | grep --color=never -o '[0-9]\ ') -gt 5000 ]]; do
      SYNC_MB=$(grep -e Dirty: /proc/meminfo | grep --color=never -o '[0-9]\ ' | awk '{$1/=1024;printf "%.2fMB\n",$1}')
      echo -en "\r${SYNC_MB}"
      sleep 1
    done
  }

  echo

  #watch -n 1 'grep -e Dirty: /proc/meminfo | grep --color=never -o '\''[0-9]\ '\'' | awk '\''{$1/=1024;printf "%.2fMB\n",$1}'\'''
  #grep -e Dirty: /proc/meminfo | grep --color=never -o '[0-9]\ ' | awk '{$1/=1024;printf "%.2fMB\n",$1}'
}

I want to port it to Powershell, and have done this so far:

sync & {
      # If the unsynced data (in kB) is greater than 50MB, then show the sync progress
      # You can replace contents in $(...) with 5001 for testing purposes
      while ( $(grep -e Dirty: /proc/meminfo | grep --color=never -o '[0-9]\ ') -gt 5000 ) {
        SYNC_MB=$(grep -e Dirty: /proc/meminfo | grep --color=never -o '[0-9]\ ' | awk '{$1/=1024;printf "%.2fMB\n",$1}')
        echo -en "\r${SYNC_MB}"
        sleep 1
      }
    }

Powershell accepts this syntax, but returns:

Id     Name            PSJobTypeName   State         HasMoreData     Location
--     ----            -------------   -----         -----------     --------  
45     Job45           BackgroundJob   Running       True            localhost 

      # If the unsynced data (in kB) is greater than 50MB, then show the sync progress
      while ( 43289423 -gt 5000 ) {
        SYNC_MB=$(grep -e Dirty: /proc/meminfo | grep --color=never -o '[0-9]\ ' | awk '{$1/=1024;printf "%.2fMB\n",$1}')
        echo -en "\r${SYNC_MB}"
        sleep 1
      }
    

It doesn't run the code, it just prints it out. The same behavior persists when you replace the $(...) with any value (e.g. 5001 to satisfy the while loop) in the while loop's condition. Any ideas?

CodePudding user response:

While superficially similar, { ... } in Bash vs. PowerShell are fundamentally different:

  • In Bash, it is a way to group statements to be executed instantly, directly in the caller's scope.

  • In PowerShell, it is a way to group statements into a script block, which is a reusable unit of code to be executed on demand, either in a child scope, via &, the call operator, or directly in the caller's scope, via ., the dot-sourcing operator.

    • If you output a script block by itself, as you did, no execution happens and its verbatim content (sans { and }) prints to the display .

By contrast, in both shells, a post-positional & serves to asynchronously launch a background job, which in PowerShell (Core only) is the implicit equivalent of using the Start-Job cmdlet.

Therefore, the following Bash call pattern:

# Bash: These are really two separate statements:
#       `sync &` to launch `sync` in the background, 
#       and `{ ...; }` to execute commands synchronously in the foreground.
# For clarity, you could place the statements on separate lines.
sync & { ...; }

corresponds to the following in PowerShell:

# PowerShell:
# Note: You may prefer `&` to `.` in order to create a child scope
#       whose variables are limited to that scope.
# Here too you could place the statements on separate lines.
sync & . { ... }

Separately, there are problems with your translation of your Bash code to PowerShell:

  • Unlike in Bash, variable assignments in PowerShell:

    • require the $ sigil there too; e.g., $SYNC_MB = ... instead of SYNC_MB=...
    • As the above juxtaposition shows, PowerShell also permits whitespace to surround =, the assignment operator.
  • Unlike in Bash, PowerShell's -gt isn't exclusively numeric: it serves as the single greater-than operator that also works with string LHS values, in which case it performs lexical comparison.

    • Therefore, in order to perform numerical comparison with an LHS that is the output from an external program such as grep, which is invariably a string, you need to cast it to an appropriate number type, typically [int]; to provide simple examples:

      (/bin/echo 10) -gt 2 # !! $false - LEXICAL comparison
      
      [int] (/bin/echo 10) -gt 2 # OK: $true - NUMERICAL comparison
      
  • Unlike in Bash, $(...), the subexpression operator is rarely needed outside expandable (double-quoted) string ("..."):

    • In variable assignments, you need no operator at all.
    • To make a command's output participate in a larger expression, it is usually better to use (...), the grouping operator
  • echo doesn't refer to the external /bin/echo utility, but is a built-in alias of the Write-Output cmdlet, which understands neither the -en options nor \-escaped escape sequences, given that PowerShell uses `, the so-called backtick, as its escape character.

    • While you could call /bin/echo via its full path, the PowerShell way is to use the Write-Host cmdlet, as shown below.

    • Alternatively, as you've discovered, PowerShell has a dedicated cmdlet for progress displays, Write-Progress, but note that its downside is that it can slow the overall operation down noticeably.

  • Due to a long-standing and highly unfortunate bug, PowerShell - up to at least 7.2.2 - doesn't properly pass " chars. embedded in external-program arguments, necessitating their manual \-escaping, even inside verbatim (single-quoted) string ('...'), such as in your awk command.

To put it all together, using & for child-scoped execution of the script block, and assuming that sync is either a PowerShell script (sync.ps1) or an external utility, placing the statements on separate lines:

sync &
& {
  while ([int] (grep -e Dirty: /proc/meminfo | grep --color=never -o '[0-9]\ ') -gt 5000 ) {
    $SYNC_MB = grep -e Dirty: /proc/meminfo | grep --color=never -o '[0-9]\ ' | awk '{$1/=1024;printf \"%.2fMB\n\",$1}'
    Write-Host -NoNewLine "`r${SYNC_MB}"
    sleep 1
  }
}

Note: If sync were a PowerShell function, the above wouldn't work, because background jobs do not share state with the caller and know nothing about its functions (except functions provided via auto-loading modules).

  • Related