How to set FromFile location in Powershell? - winforms

I am preparing a script, which needs to use some images from same folder as the script. The images are to be shown on WinForms GUI.
$imgred = [System.Drawing.Image]::FromFile("red.png")
When I run the ps1 script manually from the folder just by clicking, it loads images and shows them. Unfortuantely I do not remember exactly how I set up this, but as far as I can, it was just the default program to use for ps1 files.
When I run the script from a cmd file (to hide the cmd window), it also loads them.
But when I open with Powershell IDE and run it, I get errors and no icons are shown on my GUI.
When I open with Powershell it also fails to load them.
The only difference between those run modes I can find is with:
$scriptPath = split-path -parent $MyInvocation.MyCommand.Definition
$scriptPath #always same, the location of script
(Get-Location).Path #scriptlocation when icons loaded, system32 folder when unsuccessful load
Same behavior when doing cd $scriptPath, so the current folder is most likely not the guilty one.
I know I can write $scriptPath/red.png in each file read line (FromFile), but what I want is to define it once - the default location for FromFile - and then just have simple filename work regardless of the way I run it.
What is to be changed so the default file reading path is same as my scripts location?

Modifying the default location stack in PowerShell ($PWD) doesn't affect the working directory of the host application.
To see this in action:
PS C:\Users\Mathias> $PWD.Path
C:\Users\Mathias
PS C:\Users\Mathias> [System.IO.Directory]::GetCurrentDirectory()
C:\Users\Mathias
now change location:
PS C:\Users\Mathias> cd C:\
PS C:\> $PWD.Path
C:\
PS C:\> [System.IO.Directory]::GetCurrentDirectory()
C:\Users\Mathias
When you invoke a .NET method that takes a file path argument, like Image.FromFile(), the path is resolved relative to the latter, not $PWD.
If you want to pass a file path relative to $PWD, do:
$pngPath = Join-Path $PWD "red.png"
[System.Drawing.Image]::FromFile($pngPath)
or
[System.Drawing.Image]::FromFile("$PWD\red.png")
If you require a path relative to the executing script, in PowerShell 3.0 and newer you can use the $PSScriptRoot automatic variable:
$pngPath = Join-Path $PSScriptRoot "red.png"
If you need to support v2.0 as well, you could put something like the following at the top of your script:
if(-not(Get-Variable -Name PSScriptRoot)){
$PSScriptRoot = Split-Path $MyInvocation.MyCommand.Definition -Parent
}
When using PowerShell in interactive mode, you could configure the prompt function to have .NET "follow you around" like so:
$function:prompt = {
if($ExecutionContext.SessionState.Drive.Current.Provider.Name -eq "FileSystem"){
[System.IO.Directory]::SetCurrentDirectory($PWD.Path)
}
"PS $($executionContext.SessionState.Path.CurrentLocation)$('>' * ($nestedPromptLevel + 1)) ";
}
but I would recommend against that, just get into the habit of providing fully qualified paths instead.

Related

Batch replacing text within .xml files within a zip archive through powershell

I've posted this question before, yet was not able to find a suitable solution for my problem, hence I have been testing some more myself and have some new findings and theories as to why It might not work as intended. I hope a respectable time has passed for me to bump my question with new info attached.
I am for quite a while, in my free time, tackling a script that can batch replace external link addresses in multiple excel files within script folder. I have learned, that you can't change external links via usual powershell to excel interaction, as these values are forced to read-only. However, there is a clever way to bypass that by converting the Excel file to a .zip archive and read/change the files inside and then rename it back to excel format.
Through learning and digging around the web, I have compiled this script function that should create a backup, rename to archive and then replace desired text within, renaming the file backwards afterwards.
function Update-ExcelLinks($xlsxFile, $oldText, $newText) {
# Build BAK file name
$bakFile = $xlsxFile -ireplace [regex]::Escape(".xlsb"), ".bak"
# Build ZIP file name
$zipFile = $xlsxFile -ireplace [regex]::Escape(".xlsb"), ".zip"
# Create temporary folder
$parent = [System.IO.Path]::GetTempPath();
[string] $guid = [System.Guid]::NewGuid();
$tempFolder = Join-Path $parent $guid;
New-Item -ItemType Directory -Path $tempFolder;
# Uncomment the next line to create backup before processing XLSX file
# Copy-Item $xlsxFile $bakFile
# Rename file to ZIP
Rename-Item -Path $xlsxFile -NewName $zipFile
# Not using Expand-Archive because it changes the ZIP format
C:\7z\7za.exe x "$zipFile" -o"$tempFolder"
# Replace old text with new text
$fileNames = Get-ChildItem -Path $tempFolder -Recurse -Force -Include *.xml,*.bin.rels
foreach ($file in $fileNames)
{
(Get-Content -ErrorAction SilentlyContinue $file.PSPath) |
Foreach-Object { $_ -replace $oldText, $newText } |
Set-Content $file.PSPath
}
# Changing working folder because 7Zip option -w doesn't work
Set-Location -Path $tempFolder
# Not using Compress-Archive because it changes the ZIP format
C:\7z\7za.exe u -r "$zipFile" *.*
# Rename file back to XLSB
Rename-Item -Path $zipFile -NewName $xlsxFile
}
So far, I am able to find the desired file, rename it to .zip, extract it to a temporary folder, and according to the powershell window prompts, it updates the archive with new files. Afterwards I am left with the .zip file without any desired changes inside. The files that are responsible for external links data in the excel files are located at
wk33\Gross Qty_wk33.zip\xl\externalLinks\_rels
and are presented in the form of files named externalLink1.bin.rels and numbered onwards.
These files are essentially identical to a .xml file and are opened with either Notepad or Internet explorer through windows and contain the following:
The aim of my script is to rename the week number within "Target=" parameters from last week to current (For example wk32 to wk33). The thing is that no changes happen even though no errors are displayed, and 7zip indicates that files are packed into the zip successfully.
I have tested what happens, If I unpack the .bin.rels file and change the week number inside manually through notepad and repeat the intended script process and I can confirm that it works. When I open the file the link is correctly updated.
The last 4 steps seem to be not working as intended, even though they are correct as far as I am aware. The changes are not made and the file is not consecutively renamed back to its original .xlsb extension.
Here is the output of my powershell window upon trying to execute the function:
I've been trying for several weeks to make it work, but nothing substantial can be changed as it seems. I would appreciate any additional insights or alternatives to my code to achieve the same task.
EDIT: The function is intended to be called upon from within the working directory, so it is supposed to be used as Update-ExcelLinks 'Gross Qty_wk33.xlsb' 'wk32' 'wk33' Although I have tried calling the file via its full path as Update-ExcelLinks 'C:\Test\Gross Qty_wk33.xlsb' 'wk32' 'wk33'

WinSCP Script upload recursivelly all files matching a mask to the same folder

I have a text file that I am calling from a batch file and it is not putting files recursively in the FTP site. The folder structure has subfolders which contain the files I want to copy among many other files. The put files only copy C:\storage only. After reading the documentation and trying other method is still not copying files recursively. (no folders to be copied with the RDF from subfolders)
The folder structure is random on different PC:
C:\storage\78286.S-92A.920024*.RDF
C:\Storage\folder1\78286.S-92A.920024*.RDF
C:\Storage\storage2\folder2\78286.S-92A.920024*.RDF
There are many RDF files, but the wildcard I am interested is the one you can see above. Basically I want to select all the *.RDF (as wildcard above from all the subfolders), but do not want the subfolders to be copied to the remote.
Please see code below.
option batch continue
option confirm off
option reconnecttime 900
open ftp://companyuser:!password#ftpsite.com/
lcd "C:\storage"
put "C:\storage\78286.S-92A.920024*.RDF" "/"
put "C:\storage\*\78286.S-92A.920024*.RDF" "/"
close
exit
It's not easy to do such custom processing with WinSCP scripting only.
But with WinSCP .NET assembly from a PowerShell script, it's not difficult:
# Load WinSCP .NET assembly
Add-Type -Path "WinSCPnet.dll"
# Set up session options
$sessionOptions = New-Object WinSCP.SessionOptions -Property #{
Protocol = [WinSCP.Protocol]::Ftp
HostName = "ftp.example.com"
UserName = "username"
Password = "password"
}
$session = New-Object WinSCP.Session
Write-Host "Connecting ..."
$session.Open($sessionOptions)
$localPath = "C:\storage"
$remotePath = "/"
$wildcard = "78286.S-92A.920024*.RDF"
$localFiles = Get-ChildItem -Include $wildcard -Recurse -Path $localPath
foreach ($localFile in $localFiles)
{
Write-Host "Uploading $($localFile.FullName)..."
$session.PutFiles($localFile.FullName, $remotePath).Check()
}
Just extract a contents of WinSCP .NET assembly package along with the script (say flatupload.ps1) and run it like:
powershell -ExecutionPolicy Bypass -File flatupload.ps1
The code is partly based on WinSCP example Recursively move files in directory tree to/from SFTP/FTP server while preserving source directory structure.
See also WinSCP forum question Ignore folder structure when copying the files.
Have look at doc on https://winscp.net/eng/docs/commandline
You can use command-line winscp.com
Your script seems good, it should need to be called with winscp.com
For ftp client on Linux, mput/mget(multiple file operation), command is available but it is not available with WinScp.
You can try some work around like first create folder using mkdir command (with winscp.com) and then use synchronize option with winscp.exe to update folder content.

Running a build script after calling vcvarsall.bat from powershell

I am attempting to run the Visual Studio (developer cmd prompt) environmental variable setup batch file followed by a build script from within a Powershell script as follows:
cmd /v:on/k "(""C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat"" amd64_x86 && C:\buildscript.cmd --build-options)"
It appears however that the environmental settings established by vcvarsall.bat are not retained for the build script call. i.e. no default compiler setup, etc.
Is the /v:on combined with the /k switch not actually utilizing the same cmd session and properly delaying environmental variable expansion? Perhaps the approach is wrong ...
The problem is that when you run cmd.exe to run a batch file, the variables are set in that instance of cmd.exe, but they disappear after that instance terminates.
To work around this problem, you can use the Invoke-CmdScript function in this article:
Windows IT Pro: Take Charge of Environment Variables in PowerShell
The function is as follows:
# Invokes a Cmd.exe shell script and updates the environment.
function Invoke-CmdScript {
param(
[String] $scriptName
)
$cmdLine = """$scriptName"" $args & set"
& $Env:SystemRoot\system32\cmd.exe /c $cmdLine |
select-string '^([^=]*)=(.*)$' | foreach-object {
$varName = $_.Matches[0].Groups[1].Value
$varValue = $_.Matches[0].Groups[2].Value
set-item Env:$varName $varValue
}
}
You could add this function to your PowerShell profile or use it as a script file.
Once you have defined the function, you can run your commands:
Invoke-CmdScript "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" amd64_x86
C:\buildscript.cmd --build-options
...or whatever you need.
The article also presents a couple of functions that let you easily save and restore environment variables.

How do I use an EXE in the Chocolatey package from chocolateyInstall.ps1?

I am building a self-contained Chocolatey package. The package folder contains: app.nuspec, app.exe, app.nupkg, and the tools subfolder. The chocolateyInstall.ps1 is like this:
$packageName = 'app'
$fileType = 'exe'
$silentArgs = '/VERYSILENT'
$url = '../app.exe' # the location of the file relative to the tools folder
Install-ChocolateyPackage $packageName $fileType $silentArgs $url
When I run:
choco install app -y
I get:
Copy-Item : cannot find the path C:\ProgramData\app.exe because does not exist
How can I make this work? I've read a bit about 'create self-contained package with shims' but I don't really get how to use that? Any help? thank you
EDIT 1
I have found also this other solution here (http://patrickhuber.github.io/2015/03/19/creating-enterprise-versions-of-public-chocolatey-packages.html) that does work. So in my case that would be:
$directory = $PSScriptRoot
$packageName = 'app'
$fileType = 'exe'
$silentArgs = '/VERYSILENT'
$url = Join-Path $directory '..\app.exe'
Install-ChocolateyPackage $packageName $fileType $silentArgs $url
I was wondering what is the $PSScriptRoot variable?
For doing a Chocolatey Package that contains the exe/msi, you can use the Install-ChocolateyInstallPackage helper method, rather than the Install-ChocolateyPackage helper method. This is documented on the Chocolatey Wiki here
This works in very much the same way as the other helper method, with the exception that it doesn't want/need to download the exe/msi. It uses the path that it is provided, and installs from there.
You can find a complete example of what is required in the ChocolateyGUI package, which does a very similar thing.
The crux of it is shown below for reference:
$packageName = 'ChocolateyGUI'
$fileType = 'msi'
$silentArgs = '/quiet'
$scriptPath = $(Split-Path $MyInvocation.MyCommand.Path)
$fileFullPath = Join-Path $scriptPath 'ChocolateyGUI.msi'
Install-ChocolateyInstallPackage $packageName $fileType $silentArgs $fileFullPath
Somehow we're still missing the explanation of script- and caller-relative paths. In this case, Chocolatey is executing from
%PROGRAMDATA%\Chocolatey\choco.exe
Your script is telling it to go up one level and look for app.exe, that's
%PROGRAMDATA%\app.exe
What Gary's answer implies, by using $MyInvocation is that you need to construct this path relative to the script's location, not the caller's location. You found another method of doing that by joining the path with $PSScriptRoot.
Both of these variables are known as "Automatic variables".
$MyInvocation
Contains an information about the current command, such as the name,
parameters, parameter values, and information about how the command was
started, called, or "invoked," such as the name of the script that called
the current command.
$MyInvocation is populated only for scripts, function, and script blocks.
You can use the information in the System.Management.Automation.InvocationInfo
object that $MyInvocation returns in the current script, such as the path
and file name of the script ($MyInvocation.MyCommand.Path) or the name of a
function ($MyInvocation.MyCommand.Name) to identify the current command.
This is particularly useful for finding the name of the current script.
And
$PSScriptRoot
Contains the directory from which a script is being run.
In Windows PowerShell 2.0, this variable is valid only in script modules
(.psm1). Beginning in Windows PowerShell 3.0, it is valid in all scripts.

How to convert this sqlcmd script to an Invoke-Sqlcmd script?

I have a SQL script designed to be executed by sqlcmd, and a Command script that executes sqlcmd with the correct parameters.
I want to convert the Command script to a PowerShell script that uses Invoke-Sqlcmd instead of sqlcmd.
The SQL script, the Command script, and the new PowerShell script all live in the directory C:\Users\iain.CORP\SqlcmdQuestion.
SQL Script
The SQL script is called ExampleQuery.sql. It selects a string literal. The value of the string literal is set by sqlcmd at runtime to the value of the ComputerName sqlcmd scripting variable. The code looks like this:
SELECT '$(ComputerName)';
Command Script
The command script is called ExecQuery.cmd. It calls sqlcmd to execute ExampleQuery.sql and sets the value of the scripting variable ComputerName to the value of the environment variable COMPUTERNAME. The code looks like this:
sqlcmd -i ExampleQuery.sql -v ComputerName = %COMPUTERNAME%
When I open a command prompt, the default working directory is C:\Users\iain.CORP. I change the to the directory containing the files, and run the Command script:
cd C:\Users\iain.CORP\SqlcmdQuestion
ExecQuery.cmd
I see this output:
---------
SKYPC0083
(1 rows affected)
The script successfully selects a string literal set by sqlcmd.
PowerShell Script
The PowerShell script is called ExecQuery.ps1. It is supposed to do the same as the command script, using Invoke-Sqlcmd instead of sqlcmd. The code looks like this:
Add-PSSnapin SqlServerCmdletSnapin100
Add-PSSnapin SqlServerProviderSnapin100
Invoke-Sqlcmd -InputFile 'ExampleQuery.sql' -Variable "ComputerName = $Env:COMPUTERNAME"
When I open a PowerShell prompt, the default working directory is Z:\. I change to the directory containing the files, and run the PowerShell script:
cd C:\Users\iain.CORP\SqlcmdQuestion
.\ExecQuery.ps1
I see this output:
Invoke-Sqlcmd : Could not find file 'Z:\ExampleQuery.sql'.
At C:\Users\iain.CORP\SqlcmdQuestion\ExecQuery.ps1:4 char:14
+ Invoke-Sqlcmd <<<< -InputFile 'ExampleQuery.sql' -Variable "ComputerName = $Env:COMPUTERNAME"
+ CategoryInfo : InvalidResult: (:) [Invoke-Sqlcmd], FileNotFoundException
+ FullyQualifiedErrorId : ExecutionFailed,Microsoft.SqlServer.Management.PowerShell.GetScriptCommand
The PowerShell script raises an error because Invoke-Sqlcmd can't find the the input file in the Z:\ directory, which happens to be the default working directory.
The Command script found the script in the current working directory.
How do I make Invoke-Sqlcmd use the current working directory instead of the default working directory?
For this answer, assume that the directory C:\Users\iain.CORP\SqlcmdQuestion exists and that executing dir at that location produces the following output, as implied by the question:
Directory: C:\Users\iain.Corp\SqlcmdQuestion
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 26/09/2012 15:30 27 ExampleQuery.sql
-a--- 26/09/2012 15:30 61 ExecQuery.cmd
-a--- 26/09/2012 15:34 172 ExecQuery.ps1
PowerShell ignores the working directory by design
My question has a false premise:
How do I make Invoke-Sqlcmd use the current working directory instead of the default working directory?
The cmdlet does use the current working directory. The problem is that I didn't change the working directory at all in my PowerShell session.
In PowerShell, cd is an alias for the Set-Location cmdlet. You can prove this using the Get-Alias cmdlet:
Get-Alias cd
Output:
CommandType Name Definition
----------- ---- ----------
Alias cd Set-Location
Alex Angelopoulos explains:
[A]lthough PowerShell's location is analogous to the working directory, the location is not the same thing as the working directory. In fact, PowerShell doesn't touch the working directory.
Set-Location does not set the working directory. It sets the working location, which is a similar but distinct concept in PowerShell.
You can prove this by inspecting the working directory using the .NET property Environment.CurrentDirectory after setting the working location using cd as in the question:
cd C:\Users\iain.CORP\SqlcmdQuestion
Environment::CurrentDirectory
Output:
Z:\
I would guess this design decision was made to be consistent. The working directory would be undefined when, for example, the working location were set to a registry hive.
Invoke-Sqlcmd violates this design principle
Invoke-Sqlcmd violates PowerShell's general design principle to use the working location rather than the working directory. Most cmdlets use the working location to resolve relative paths, but Invoke-Sqlcmd is an exception.
Using the ILSpy disassembler and a little intuition to inspect the containing assembly Microsoft.SqlServer.Management.PSSnapins, I believe I have found the reason for the error.
I believe that the cmdlet's parameter -InputFile is implemented by the method IncludeFileName. ILSpy's disassembly of the method looks like this:
// Microsoft.SqlServer.Management.PowerShell.ExecutionProcessor
public ParserAction IncludeFileName(string fileName, ref IBatchSource pIBatchSource)
{
if (!File.Exists(fileName))
{
ExecutionProcessor.sqlCmdCmdLet.TerminateCmdLet(new FileNotFoundException(PowerShellStrings.CannotFindPath(fileName), fileName), "ExecutionFailureException", ErrorCategory.ParserError);
return ParserAction.Abort;
}
BatchSourceFile batchSourceFile = new BatchSourceFile(fileName);
pIBatchSource = batchSourceFile;
return ParserAction.Continue;
}
Invoke-Sqlcmd uses the .NET method File.Exists to check whether the specified input file exists. The method's documentation remarks that relative paths are resolved using the working directory:
The path parameter is permitted to specify relative or absolute path
information. Relative path information is interpreted as relative to
the current working directory. To obtain the current working
directory, see GetCurrentDirectory.
This suggests that File.Exists would return false in this case, which would cause the error message seen in the question. You can prove this by executing the method directly from the prompt:
cd C:\Users\iain.CORP\SqlcmdQuestion
[IO.File]::Exists('ExecQuery.sql')
Output:
False
The method returns false, so the cmdlet terminates with a 'file not found' error.
You can work around the unusual behavior
There are two workarounds for Invoke-Sqlcmd using the working directory instead of the working location to resolve relative paths:
Always use an absolute path as the value of the -InputFile parameter. CandiedCode's answer shows how to do this.
Set the working directory and use a relative path.
I solved the problem without side-effects by modifying ExecQuery.ps1 like this:
Add-PSSnapin SqlServerCmdletSnapin100
Add-PSSnapin SqlServerProviderSnapin100
$RestoreValue = [Environment]::CurrentDirectory
[Environment]::CurrentDirectory = Get-Location
Invoke-Sqlcmd -InputFile 'ExampleQuery.sql' -Variable "ComputerName = $Env:COMPUTERNAME"
[Environment]::CurrentDirectory = $RestoreValue
I see this output:
Column1
-------
SKYPC0083
Success!
The new script sets the working directory to match the working location before executing Invoke-Sqlcmd. To avoid unintended side-effects of changing the working directory, the scrtipt restores the working directory value before completing.
Setting the current directory is described in this Channel 9 thread. The example there uses the Directory.SetCurrentDirectory method, but I find it simpler to set the property directly.
You could fully qualify the Inputfile location:
Invoke-Sqlcmd -InputFile 'C:\Users\iain.CORP\SqlcmdQuestion\ExampleQuery.sql' -Variable "ComputerName = $Env:COMPUTERNAME"
And use a variable to drive the script location:
$FileLocation = 'C:\Users\iain.CORP\SqlcmdQuestion\'

Resources