1

I was used to a few command line tricks in Windows that increased my productivity a lot.

Now I am told that I should move to PowerShell because it's more POWERful. It took me a while to get a little bit hang of it (objects, piping, etc.), and there are a lot of great tutorials on how to get a few things done. However, some (relatively) basic trick still puzzle me. For instance, what is the equivalent of the FOR structure in PowerShell?

For example,

FOR %i IN (*.jpg) DO Convert %i -resize 800x300 resized/%i

The above line takes all of photos in a folder and uses the ImageMagick's Convert tool to resize the images and restores the resized imaged in a sub-folder called RESIZED.

In PowerShell I tried the command:

Dir ./ | foreach {convert $_.name -resize 800x300 resized/$_name}

This can't work despite all of the googling around I did. What is missing?

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
  • 1
    At the end of the `foreach` block you use `$_name` - is that a typo? It should be `$_.name` – arco444 Feb 07 '17 at 11:30
  • 1
    Your error message(s) might help! But to take an early stab at the answer: PowerShell doesn't understand what "convert" is! You may have to refer to the executable directly e.g. start with `& C:\path\to\exe\convert.exe -?` – gvee Feb 07 '17 at 13:36
  • 1
    [`convert` -> `.\convert`](http://stackoverflow.com/a/31238579) – user4003407 Feb 07 '17 at 16:23

3 Answers3

2

Note that / rather than \ is used as the path separator in this answer, which works on Windows too and makes the code compatible with the cross-platform PowerShell Core editions.

tl;dr:

$convertExe = './convert' # adjust path as necessary
Get-ChildItem -File -Filter *.jpg | ForEach-Object { 
  & $convertExe $_.Name -resize 800x300 resized/$($_.Name)
}

Read on for an explanation and background information.


The equivalent of:

FOR %i IN (*.jpg)

is:

Get-ChildItem -File -Filter *.jpg

or, with PowerShell's own wildcard expressions (slower, but more powerful):

Get-ChildItem -File -Path *.jpg    # specifying parameter name -Path is optional

If you're not worried about restricting matches to files (as opposed to directories), Get-Item *.jpg will do too.

While dir works as a built-in alias for Get-ChildItem, I recommend getting used to PowerShell's own aliases, which follow a consistent naming convention; e.g., PowerShell's own alias for Get-ChildItem is gci

Also, in scripts it is better to always use the full command names - both for readability and robustness.

As you've discovered, to process the matching files in a loop you must pipe (|) the Get-ChildItem command's output to the ForEach-Object cmdlet, to which you pass a script block ({ ... }) that is executed for each input object, and in which $_ refers to the input object at hand.

(foreach is a built-in alias for ForEach-Object, but note that there's also a foreach statement, which works differently, and it's important not to confuse the two.)

There are 2 pitfalls for someone coming from the world of cmd.exe (batch files):

  • In PowerShell, referring to an executable by filename only (e.g., convert) does not execute an executable by that name located in the current directory, for security reasons.

    • Only executables in the PATH can be executed by filename only, and unless you've specifically placed ImageMagick's convert.exe in a directory that comes before the SYSTEM32 directory in the PATH, the standard Windows convert.exe utility (whose purpose is to convert FAT disk volumes to NTFS) will be invoked.
      Use Get-Command convert to see what will actually execute when you submit convert; $env:PATH shows the current value of the PATH environment variable (equivalent of echo %PATH%).

    • If your custom convert.exe is indeed in the current directory, invoke it as ./convert - i.e., you must explicitly reference its location.

    • Otherwise (your convert.exe is either not in the PATH at all or is shadowed by a different utility) specify the path to the executable as needed, but note that if you reference that path in a variable or use a string that is single- or double-quoted (which is necessary if the path contains spaces, for instance), you must invoke with &, the call operator; e.g.,
      & $convertExe ... or & "$HOME/ImageMagic 2/convert" ...

  • PowerShell sends objects through the pipeline, not strings (this innovation is at the heart of PowerShell's power). When you reference and object's property or an element by index as part of a larger string, you must enclose the expression in $(...), the subexpression operator:

    • resized/$($_.Name) - Correct: property reference enclosed in $(...)
    • resized/$_.Name - !! INCORRECT - $_ is stringified on its own, followed by literal .Name
      • However, note that a stand-alone property/index reference or even method call does not need $(...); e.g., $_.Name by itself, as used in the command in the question, does work, and retains its original type (is not stringified).
    • Note that a variable without property / index access - such as $_ by itself - does not need $(...), but in the case at hand $_ would expand to the full path. For the most part, unquoted tokens that include variable references are treated like implicitly double-quoted strings, whose interpolation rules are summarized in this answer of mine; however, many additional factors come into play, which are summarized here; edge cases are highlighted in this question.
    • At the end of the day, the safest choice is to double-quote strings that contain variable references or subexpressions:
      • "resized/$($_.Name)" - SAFEST
Community
  • 1
  • 1
mklement0
  • 382,024
  • 64
  • 607
  • 775
  • 1
    Thank you for this comprehensive answer. You are right about enclosing the output filename into a $(....) sub-expression; this was the culprit. Still, I don't understand why I need to do this only for the 'OUTPUT' filename. $_.name works well as the 'INPUT' filename! – Faustin Gashakamba Feb 14 '17 at 13:20
  • @FaustinGashakamba: `$_.name` works because it is a _stand-alone_ expression: there are no additional surrounding characters that would make it an _expandable string_. Please see if my update to the last group of bullet points helps. The rules _are_ tricky, unfortunately; again [this answer](http://stackoverflow.com/a/41254359/45375) details them. – mklement0 Feb 14 '17 at 13:30
0

Use:

Get-ChildItem | foreach {convert $_.name -resize 800x300 resized/$($_.name)}

Or, perhaps, you need to pass the full name (with path), also showing a shorter syntax (using aliases):

gci | % {convert $_.fullname -resize 800x300 resized/$($_.name)}

Also, you might want to supply the full path to the executable.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
4c74356b41
  • 69,186
  • 6
  • 100
  • 141
  • 1
    `&` is only needed when the command name would otherwise cause PowerShell to start parsing in _expression_ mode, such as when your command name is stored in a variable or is specified as a quoted string. E.g., the following commands only work if you prepend `&` to the command-name token: `'cmd' /c ver`, `"cmd" /c ver`, `$c = 'cmd'; $c /c ver`; by contrast, `cmd /c ver` and `& cmd /c ver` act the same. – mklement0 Feb 07 '17 at 17:11
0

Revised based on comments given below

There are many applications with the name "Convert". If I do

Get-Command Convert

on my computer. It shows me an app that is part of the Windows system. If PowerShell is running the wrong app on you, it's never going to work.

The solution will be to point PowerShell at the convert tool inside the ImageMagick program folder. A Google search on "ImageMagick PowerShell" will lead you to lots of people who have faced the same problem as you.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Walter Mitty
  • 18,205
  • 2
  • 28
  • 58
  • You have a point about `convert` potentially referring to a different command, but unless you've explicitly defined an _alias_ named `convert`, there is no risk of confusion with PowerShell's `Convert-*` cmdlets. (A _function_ named `convert` would also shadow an external utility by that name). – mklement0 Feb 07 '17 at 17:14
  • Well, if you type in the command up to the word "Convert" and then hit the TAB key, PS changes it to Convert-Path", so I think it is parsing that verb as a possible cmdlet. – Walter Mitty Feb 07 '17 at 18:14
  • Yes, but _tab completion_, which relates to _editing_ a command and has nothing to do with executing a command named `convert` - the command is interpreted as specified, there are no magic behind-the-scenes completions at _execution time_. – mklement0 Feb 07 '17 at 18:17
  • OK, I think I may have figured out what's happening on my computer. There is a DOS command named "Convert", and it's being invoked if I just give a convert command. I guess that whether thisd gets invoked or the tool from ImageMagick depends on the search order. – Walter Mitty Feb 07 '17 at 18:49
  • Yes, but just to clarify, it is `$env:SYSTEMROOT\system32\convert.exe`, a standard utility for converting FAT volumes to NTFS that is an NT console application (not a DOS command). Can I suggest you remove the part about confusion with the `Convert-*` cmdlets from your answer? – mklement0 Feb 07 '17 at 19:39