2

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?

MilkyDeveloper
  • 608
  • 1
  • 6
  • 13
  • 2
    `sync` will be executed as a job (since `&` is the [Background operator `&`](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_operators?view=powershell-7.2#background-operator-) in PS Core) and then what follows is a script block which gets displayed to the console as it is. It's not clear what you want to accomplish with the syntax `sync & { ... }` – Santiago Squarzon Apr 15 '22 at 18:40
  • Yes, I'd like to run the commands concurrently, so that while `sync` is running, it prints out the remaining MBs to write. – MilkyDeveloper Apr 15 '22 at 18:42

1 Answers1

1

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).

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • Thanks :D. This is a great answer! I ended up replacing Write-Host with Write-Progress, like https://pastebin.com/GNwuz68n – MilkyDeveloper Apr 15 '22 at 20:33
  • Glad to hear it, @MilkyDeveloper. Good point re `Write-Progress`; I've added it to the answer (note the potential performance caveat). Please also see my update re the pitfall re `-gt`, which in PowerShell performs lexical comparison with a string LHS, which is why an `[int]` cast is needed. – mklement0 Apr 15 '22 at 20:48
  • @MilkyDeveloper, just looked at your link: To avoid the `-gt` pitfall, define your `getSyncKB` function as follows: `function getSyncKB { [int] (grep -e Dirty: /proc/meminfo | grep --color=never -o '[0-9]\+') }` - note that there's also no need for `return`, which in PowerShell is only needed to explicitly exit a scope. PowerShell scripts and functions do not have return values, they emit to the success output stream, PowerShell's analog to stdout, as Bash scripts and functions do - see [this answer](https://stackoverflow.com/a/69792182/45375) for details. – mklement0 Apr 15 '22 at 20:55