I am several layers deep into a problem with a PowerShell Script that we use to manage user accounts. When users move between locations, we move their ADUser to a new OU, and move their data between servers.
While researching how to copy their home folder, it was determined that there is no easy way to copy files using PowerShell and exclude certain directories. As a result we are calling robocopy.exe and using /XD. This also lets us start the copy, but have the script continue on to the next user.
We are attempting to enhance our script by moving or deleting the older folder depending on the user type. I created a PS1 file that I would like to call so that the script can continue to the next user immediately and not wait for the copy to finish. The PS1 file looks like this:
Param(
[string]$source,
[string]$destination,
[string]$oldStaff
)
robocopy $source $destination
move $source $oldStaff
When I attempt to call this from our script in the following way:
start powershell.exe .\Move-WCFolders.ps1 $source $destination $oldStaff
I get the following error message:
Start-Process : A positional parameter cannot be found that accepts
argument
'<%Removed server name before posting to Stack Overflow%>'. At
line:1 char:1
+ start powershell.exe .\Move-WCFolders.ps1 $source $Description
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [Start-Process], ParameterBindingException
+ FullyQualifiedErrorId : PositionalParameterNotFound,Microsoft.PowerShell.Commands.StartProcessCommand
I'm sure that there is likely a far better way to achieve what I am trying to achieve. It doesn't feel 'clean' to need to start robocopy, or to have to call things in an external script. The end goal is to copy folders while excluding directories, move the folder when the copy is done, but not have to wait for the copy to complete to continue the script.
Try using Start-Job. It's easier than you think, and you'll be able to capture the results for later:
$job = Start-Job -FilePath '.\Move-WCFolder.ps1' -ArgumentList $source, $destination, $oldStaff
# do more processing here
# . . .
# wait for the job to finish and output the results
$job | Receive-Job
start is an alias for Start-Process
The error is because the arguments at the end.
AFAIK, you will most likely want to turn them into a single string to pass to powershell.
You may want to try something like this:
start powershell.exe -File .\Move-WCFolders.ps1 -ArgumentList "$source $destination $oldStaff"
You will want to make sure you have the escaping right, so it may end up looking like this:
start powershell.exe -File .\Move-WCFolders.ps1 -ArgumentList "`"$source`" `"$destination`" `"$oldStaff`""
You could also investigate using Start-Job.
Related
I have a curious case that I cannot fathom the reason for...
Please know I am a novice to PowerShell.
I am working on a PowerShell menu system to help automate building out new computers in my environment. I have a PS1 file that holds the script for an app install. When I use the script to reference this I am able to run it and have no issue. However, when I try inserting this into a function and referencing it does not.
This works:
4 # Microsoft Office 32-bit
{
Write-Host "`nMicrosoft Office 32-bit..." -ForegroundColor Yellow
# {installMS32Bit}
Invoke-Expression "cmd /c start powershell -NoExit -File '\\**SERVERPATH**\menuItems\ms_office\32-bit\install.ps1'"
Start-Sleep -seconds 2
}
This does not:
function installMS32Bit(){
Invoke-Expression "cmd /c start powershell -NoExit -File '\\**SERVERPATH**\menuItems\ms_office\32-bit\install.ps1'"
}
}
4 # Microsoft Office 32-bit
{
Write-Host "`nMicrosoft Office 32-bit..." -ForegroundColor Yellow
{installMS32Bit}
Start-Sleep -seconds 2}
install.ps1 file:
# Copy MS Office uninstall and setup to local then run and install 32-bit Office
Copy-Item -Path '\\**SERVERPATH**\menuItems\ms_office\setup.exe' -Destination 'C:\temp\' -Force
Copy-Item -Path '\\**SERVERPATH**\menuItems\ms_office\uninstall.xml' -Destination 'C:\temp\' -Force
Copy-Item -Path '\\**SERVERPATH**\menuItems\ms_office\32-bit\Setup.exe' -Destination 'C:\temp' -Force
Invoke-Expression ("cmd /c 'C:\temp\setup.exe' /configure 'C:\temp\uninstall.xml'")
Start-Process -FilePath 'C:\temp\Setup.exe'
Secondary question and a little explanation for Invoke-Expression...
I like to see progress and like to have secondary windows open to monitor the new process being run. I was unable to find a solution with a persistent window that worked for me to do this without Invoke-Expression.
If there is a better way to do this in PowerShell I am all ears!
{installMS32Bit}
As Mathias points out in a comment on the question, this statement doesn't call your function, it wraps it in a script block ({ ... })[1], which is a piece of reusable code (like a function pointer, loosely speaking), for later execution via &, the call (execute) operator.
To call your function, just use its name (by itself here, given that there are no arguments to pass): installMS32Bit
Invoke-Expression should generally be avoided; definitely don't use it to invoke an external program, as in your attempts.
Additionally, there's generally no need to call an external program via cmd.exe (cmd /c ...), just invoke it directly.
For instance, replace the last Invoke-Epression call from your question with:
# If the EXE path weren't quoted, you wouldn't need the &
& 'C:\temp\setup.exe' /configure 'C:\temp\uninstall.xml'
I like to see progress and like to have secondary windows open to monitor the new process being run. I was unable to find a solution with a persistent window that worked for me to do this without Invoke-Expression.
(On Windows), Start-Process by default executes a console application in a new window (unless you specify -NoNewWindow), asynchronously (unless you specify -Wait).
You cannot pass a .ps1 script directly to Start-Process (it will be treated like a document to open rather than an executable to call), but you can pass it to PowerShell's CLI via the -File parameter:
Start-Process powershell.exe '-File install.ps1'
The above is short for:
Start-Process -FilePath powershell.exe -ArgumentList '-File install.ps1'
That is, PowerShell will execute the following in a new window:
powershell.exe -File install.ps1
[1] Since you're not assigning the script block being created to a variable, it is implicitly output (printed to the display, in the absence of a redirection); a script block stringifies by its literal contents, excluding the enclosing { and }, so string installMS32Bit will print to the display.
I am trying to streamline how I execute some scripts I wrote by setting up a function and alias to run them. I currently have functions to change my directory to where the scripts need to be run, but when I try to run the script itself I get the following error:
C:\Users\me\Desktop\BoB : The term 'C:\Users\me\Desktop\BoB' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try
again.
At line:1 char:1
+ C:\Users\me\Desktop\BoB Tools\folderScannerV0.4.ps1
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (C:\Users\jteit\Desktop\BoB:String) [], CommandNotFoundException
+ FullyQualifiedErrorId : CommandNotFoundException
The function is:
function run-scanner { "& 'C:\Users\me\Desktop\BoB Tools\folderScannerV0.4.ps1'" | Invoke-Expression }
I've tried a few variations based on other answers I've found, but I keep getting the same error. I would prefer to not remove the space on the path because other scripts use it successfully. Running the script from the ISE gives me no problems.
Ideally I would like the function to also allow me to have the script run on the folders I would like without changing the working directory (each script works on a particular set of files that are in a static location but some of them use $PWD to get the folders in the location).
For example in my $profile file I have this function: function go-to-temp {cd "C:\Users\me\Desktop\Bob Tools\To be Formatted\Temp"} which I run before I execute the above script. I would like them rolled into a single command without my working directory changing (which would render the go-to-temp function redundant.
What am I doing wrong?
There is no reason to run your script through Invoke-Expression.
Unless your script relies on $PWD, then you should be able to execute it with the call operator: &. As the other poster mentioned, you can use dot-sourcing (.) if you need the variables the script generates, but this will import all global objects (aliases, variables, functions) to your current scope. If it does rely on $PWD, you can utilize Start-Process with -WorkingDirectory to avoid changing where you're at.
function Start-Scanner {
& "$HOME\Desktop\BoB Tools\folderScannerV0.4.ps1"
}
or
function Start-Scanner {
$startArgs = #{
FilePath = "$PSHOME\powershell.exe"
ArgumentList = '-File', "`"$HOME\Desktop\BoB Tools\folderScannerV0.4.ps1`""
WorkingDirectory = "$HOME\Desktop\BoB Tools"
NoNewWindow = $true
Wait = $true
}
Start-Process #startArgs
}
You can just use dot-sourcing for this:
function run-scanner { . 'C:\Users\me\Desktop\BoB Tools\folderScannerV0.4.ps1' }
I have 6+ scripts and growing to run on a folder, what is the best way to have them run one after the other.
I have seen and tried this thread - it did not work unfortunately.
How to run multiple Scripts one by one in a powershell script
In a master .ps file - I have put links to the power shell scripts that need to be run in order
Run Scripts in order
'C:\Users\WP\Desktop\Scripts\1.ps1'
'C:\Users\WP\Desktop\Scripts\2.ps1'
'C:\Users\WP\Desktop\Scripts\3.ps1'
etc
This did not work either. Your help is appreciated - I have searched all over and can't seem to fix this issue.
Revised: I believe I will have to wait for each power shell script to finish before running the next one - as I have had errors when I tried to run 3 scripts one after the other - nothing happened when the scripts were run
Final -
I thank you all for your help - to keep things simple this is what I have done
My folder has the scripts below - I have then created a Master.ps1 with the code below inside it:
&"$PSScriptroot\1.ps1"
&"$PSScriptroot\2.ps1"
&"$PSScriptroot\3.ps1"
&"$PSScriptroot\4.ps1"
&"$PSScriptroot\5.ps1"
&"$PSScriptroot\6.ps1"
I have then run the Master.ps1 on the folder with all the files - and it does the job so far.
If you don't want to hard code the path, you can make Master.ps1 like this:
&"$PSScriptroot\1.ps1"
&"$PSScriptroot\2.ps1"
&"$PSScriptroot\3.ps1"
And it will look for those scripts in the same directory where it is.
Simply create a text file, using any code editor or text editor, and use the following example batch script:
start /min powershell.exe C:\your folder\script1.ps1
start /min powershell.exe C:\your folder\script2.ps1
Save it as a script.bat and open it. This will make two powershell scripts run at the same time.
To get the path that your script is in you can do this:
$MyInvocation.MyCommand.Definition
That will show something like 'C:\Users\WP\Desktop\Scripts\Master.ps1'. From that we can do a Split-Path to get just the folder, and run Get-ChildItem on the folder to get a list of files. We'll probably want to exclude the master script, so that we don't end up in a recursive loop, so that would look something like:
$ScriptPath = Split-Path $MyInvocation.MyCommand.Definition
Get-ChildItem "$ScriptPath\*.ps1" | Where{$_.FullName -ne $MyInvocation.MyCommand.Definition}
Then we just run those through a ForEach-Object loop, and invoke the script with the call operator & as such:
$ScriptPath = Split-Path $MyInvocation.MyCommand.Definition
Get-ChildItem "$ScriptPath\*.ps1" | Where{$_.FullName -ne $MyInvocation.MyCommand.Definition} | ForEach-Object { & $_.FullName }
Edit: Hm, that wasn't filtering right. Here's a re-write that does filter out the master script correctly.
$Master = Split-Path $MyInvocation.MyCommand.Definition -Leaf
$ScriptPath = Split-Path $MyInvocation.MyCommand.Definition
Get-ChildItem "$ScriptPath\*.ps1" | Where{$_.Name -ne $Master} | ForEach-Object { & $_.FullName }
Are you hard coding the paths to the files in the master file?
In this case something like this should work
Invoke-Expression "C:\Users\WP\Desktop\Scripts\1.ps1"
Invoke-Expression "C:\Users\WP\Desktop\Scripts\2.ps1"
Invoke-Expression "C:\Users\WP\Desktop\Scripts\3.ps1"
The reason why the powershell screen pops up is cuz you dont have the argument -NoExit set. Please use the following as an example to see if your code is running properly:
Start-Process "$pshome\powershell.exe" -ArgumentList "-NoExit", "-Command '& script'" -wait
Sorry for the late response but I also ran into the issue and thats how I was able to resolve it to make sure it was work.
Also I changes the file names as follows:
$Scripts =
#(
".\C:\Scripts\First.ps1"
".\C:\Scripts\Second.ps1"
".\C:\Scripts\Third.ps1"
);
To run multiple scripts sequentially you can use the -Wait parameter on Start-Process like so:
$scriptsList =
#(
'C:\Users\WP\Desktop\Scripts\1.ps1'
'C:\Users\WP\Desktop\Scripts\2.ps1'
'C:\Users\WP\Desktop\Scripts\3.ps1'
)
foreach($script in $scriptsList)
{
Start-Process -FilePath "$PSHOME\powershell.exe" -ArgumentList "-command '& $script'" -Wait
}
PowerShell will wait for the current script to finish before running the next script
I'm trying to create a Jenkins Parametrized build using the file parameter.
The user will select build with parameters, and will supply a text document with a list of items (one per line).
I would like to try and read this list in my powershell script to then go off and make some service calls (I've got that bit sussed).
I'm struggling with getting my powershell script to read the file that has been passed into Jenkins, here is what I have so far
$ids = Import-Csv input.txt
$array = #()
Write-Host $array.Length
foreach($id in $ids){
Write-Host $id.id
$array += $id
Write-Host "Array now has" $array.Length "items"
}
I know the script is running OK as I can run in Powershell ISE and pass in the absolute path of the csv/txt file.
For reference, the content of input.txt is like this:
id
2884430041011214,
9751297519392363,
lfsdkjgskdjflgsdjfg
Here are the two outputs I get for
Powershell ISE (as expected):
0
2884430041011214
Array now has 1 items
9751297519392363
Array now has 2 items
lfsdkjgskdjflgsdjfg
Array now has 3 items
Jenkins (not what I'm hoping for)
Started by user anonymous
Building in workspace C:\Users\jonec34\.jenkins\jobs\getParty\workspace
[workspace] $ powershell.exe -NonInteractive -ExecutionPolicy ByPass "& 'C:\Users\jonec34\AppData\Local\Temp\hudson4035619350462822370.ps1'"
Import-Csv : Could not find file 'C:\Users\jonec34\.jenkins\jobs\getParty\workspace\input.txt'.
At C:\Users\jonec34\AppData\Local\Temp\hudson4035619350462822370.ps1:1 char:8
+ $ids = Import-Csv input.txt
+ ~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OpenError: (:) [Import-Csv], FileNotFoundException
+ FullyQualifiedErrorId : FileOpenFailure,Microsoft.PowerShell.Commands.ImportCsvCommand
0
Finished: SUCCESS
Can anyone tell me how I can do this please. I've also tried putting Param($file) at the start of the script, and using ${WORKSPACE} but either they're not working or I'm not understanding it properly. Any help greatly appreciated.
Thanks
Right, I've found where I was going wrong...
In my file parameter, I hadn't set a file location (EG I'd left it blank), but I entered in '\input.txt' and low and behold I get
Building in workspace C:\Users\jonec34\.jenkins\jobs\getParty\workspace
Copying file to \input.txt
[workspace] $ powershell.exe -NonInteractive -ExecutionPolicy ByPass "& 'C:\Users\jonec34\AppData\Local\Temp\hudson6216657038417634126.ps1'"
getting requests from file
0
2884430041011214
Array now has 1 items
9751297519392363
Array now has 2 items
lfsdkjgskdjflgsdjfg
Array now has 3 items
Finished: SUCCESS
I am fairly new to powershell and I am trying to create a script that executes a .exe file. I can execute them on my machine no problem because the folder path is hard coded. The problem is that if I shift this script to another computer, the .exe it calls might be located in a different folder structure. Example
My computer:
D:\Folder1\subfolder\RunMe.exe
Client computer might be
D:\RunMe\subfolder\RunMe.exe
I just need it to execute the RunMe.exe no matter where it is. Is there a way to do this in powershell?
# 1. Get the location of RunMe.exe
$RunMe = Get-ChildItem -Path d:\* -Include RunMe.exe -Recurse;
# 2. Invoke RunMe.exe
Start-Process -FilePath $RunMe[0].FullName -Wait -NoNewWindow;