6

I need to only search the 1st line and last line in a text file to find a "-" and remove it. How can I do it? I tried select-string, but I don't know to find the 1st and last line and only remove "-" from there.

Here is what the text file looks like:

 % 01-A247M15 G70 
N0001 G30 G17 X-100 Y-100 Z0
N0002 G31 G90 X100 Y100 Z45
N0003 ; --PART NO.:  NC-HON.PHX01.COVER-SHOE.DET-1000.050 
N0004 ; --TOOL:  8.55 X .3937 
N0005 ;  
N0006  % 01-A247M15 G70 

Something like this?

$1 = Get-Content C:\work\test\01.I

$1 | select-object -index 0, ($1.count-1)
animuson
  • 53,861
  • 28
  • 137
  • 147
Blitzcrank
  • 917
  • 2
  • 10
  • 19

8 Answers8

6

try:

$txt = get-content c:\myfile.txt
$txt[0] = $txt[0] -replace '-'
$txt[$txt.length - 1 ] = $txt[$txt.length - 1 ] -replace '-'
$txt | set-content c:\myfile.txt
CB.
  • 58,865
  • 9
  • 159
  • 159
6

Ok, so after looking at this for a while, I decided there had to be a way to do this with a one liner. Here it is:

(gc "c:\myfile.txt") | % -Begin {$test = (gc "c:\myfile.txt" | select -first 1 -last 1)} -Process {if ( $_ -eq $test[0] -or $_ -eq $test[-1] ) { $_ -replace "-" } else { $_ }} | Set-Content "c:\myfile.txt"

Here is a breakdown of what this is doing:

First, the aliases for those now familiar. I only put them in because the command is long enough as it is, so this helps keep things manageable:

  1. gc means Get-Content
  2. % means Foreach
  3. $_ is for the current pipeline value (this isn't an alias, but I thought I would define it since you said you were new)

Ok, now here is what is happening in this:

  1. (gc "c:\myfile.txt") | --> Gets the content of c:\myfile.txt and sends it down the line
  2. % --> Does a foreach loop (goes through each item in the pipeline individually)
  3. -Begin {$test = (gc "c:\myfile.txt" | select -first 1 -last 1)} --> This is a begin block, it runs everything here before it goes onto the pipeline stuff. It is loading the first and last line of c:\myfile.txt into an array so we can check for first and last items
  4. -Process {if ( $_ -eq $test[0] -or $_ -eq $test[-1] ) --> This runs a check on each item in the pipeline, checking if it's the first or the last item in the file
  5. { $_ -replace "-" } else { $_ } --> if it's the first or last, it does the replacement, if it's not, it just leaves it alone
  6. | Set-Content "c:\myfile.txt" --> This puts the new values back into the file.

Please see the following sites for more information on each of these items:

Get-Content uses
Get-Content definition
Foreach
The Pipeline
Begin and Process part of the Foreach (this are usually for custom function, but they work in the foreach loop as well)
If ... else statements
Set-Content

So I was thinking about what if you wanted to do this to many files, or wanted to do this often. I decided to make a function that does what you are asking. Here is the function:

function Replace-FirstLast {
    [CmdletBinding()]
    param(
        [Parameter( `
            Position=0, `
            Mandatory=$true)]
        [String]$File,
        [Parameter( `
            Position=1, `
            Mandatory=$true)]
        [ValidateNotNull()]
        [regex]$Regex,
        [Parameter( `
            position=2, `
            Mandatory=$false)]
        [string]$ReplaceWith=""
    )

Begin {
    $lines = Get-Content $File
} #end begin 

Process {
    foreach ($line in $lines) {
        if ( $line -eq $lines[0]  ) {
            $lines[0] = $line -replace $Regex,$ReplaceWith 
        } #end if
        if ( $line -eq $lines[-1] ) {
            $lines[-1] = $line -replace $Regex,$ReplaceWith
        }
    } #end foreach
}#End process

end {
    $lines | Set-Content $File
}#end end

} #end function

This will create a command called Replace-FirstLast. It would be called like this:

Replace-FirstLast -File "C:\myfiles.txt" -Regex "-" -ReplaceWith "NewText"

The -Replacewith is optional, if it is blank it will just remove (default value of ""). The -Regex is looking for a regular expression to match your command. For information on placing this into your profile check this article

Please note: If you file is very large (several GBs), this isn't the best solution. This would cause the whole file to live in memory, which could potentially cause other issues.

Nick
  • 4,302
  • 2
  • 24
  • 38
  • good answer, but be aware that if the files get huge it would be better to use `get-content -First 1` and `... -Last 1` in the begin statement so you don't keep a xxGB file in memory while processing. – Frode F. Jan 18 '13 at 17:52
  • True, but then we are back at the problem of it doesn't pass the whole file back, just the first and last line. – Nick Jan 18 '13 at 18:18
  • no. I said in the begin statement `foreach -begin { ..here... }`. :) – Frode F. Jan 18 '13 at 18:20
  • Ahh, my mistake, I wasn't think what my own code did. I'll make the change. – Nick Jan 18 '13 at 18:23
  • I actually meant `$first = get-content $path -first 1` and `$last = get-content $path -last 1`, but still an improvement :P I'm not 100% sure but with `select` you still work through the whole file to find the lines. – Frode F. Jan 18 '13 at 18:28
  • 1
    `Get-content` doesn't have a `-first` or a `-last` parameter, at least in PSv2. – Nick Jan 18 '13 at 18:29
  • 1
    they are actually called `-Totalcount`(first) and `-Tail`(last). first and last are aliases that work(at least in 3.0) :) edit: oh, tail is introduced in 3.0, but the question is tagged with that anyways. – Frode F. Jan 18 '13 at 18:31
  • Ahh, in 2.0 there is no tail option, and the `-first` and `-last` throw a no parameter error. There isn't an option I saw to read from the end of a file, so while `-totalcount(1)` would give the first line, there isn't anything to give the last line. – Nick Jan 18 '13 at 18:34
  • I added a warning about using this with large files. – Nick Jan 18 '13 at 18:39
1

You can use the select-object cmdlet to help you with this, since get-content basically spits out a text file as one huge array.

Thus, you can do something like this

get-content "path_to_my_awesome_file" | select -first 1 -last 1

To remove the dash after that, you can use the -Replace switch to find the dash and remove it. This is better than using System.String.Replace(...) method because it can match regex statements and replace whole arrays of strings too!

That would look like:

# gc = Get-Content. The parens tell Powershell to do whatever's inside of it 
# then treat it like a variable.
(gc "path_to_my_awesome_file" | select -first 1 -last 1) -Replace '-',''
animuson
  • 53,861
  • 28
  • 137
  • 147
Carlos Nunez
  • 2,047
  • 1
  • 18
  • 20
  • I was thinking like this too, the only problem I found is that he needs the entire array. If you try to pipe this back into the file, it will just have the first and last lines, not the whole file with a changed first and last line. – Nick Jan 18 '13 at 17:07
  • 1
    Also, it should be `gc` as the alias to `Get-Content`. `gci` is the alias to `Get-ChildItem` – Nick Jan 18 '13 at 17:25
  • Thanks for correcting me ; I use `cat` so often, I forget the entire name of the cmdlet sometimes. :) – Carlos Nunez Jan 18 '13 at 20:28
  • No problem, I would have just edited it myself, but I don't have the rep to only change two letters. – Nick Jan 18 '13 at 21:01
1

If your file is very large you might not want to read the whole file to get the last line. gc -Tail will get the last line very quickly for you.

function GetFirstAndLastLine($path){

    return  New-Object PSObject -Property @{        
        First = Get-Content $path -TotalCount 1
        Last = Get-Content $path -Tail 1
        }
}

GetFirstAndLastLine "u_ex150417.log"

I tried this on a 20 gb log file and it returned immediately. Reading the file takes hours.

You will still need to read the file if you want to keep all excising content and you want only to remove from the end. Using the -Tail is a quick way to check if it is there.

I hope it helps.

Jeno Laszlo
  • 2,023
  • 18
  • 36
0

A cleaner answer to the above:

$Line_number_were_on = 0
$Awesome_file = Get-Content "path_to_ridiculously_excellent_file" | %{ 
    $Line = $_ 
    if ($Line_number_were_on -eq $Awesome_file.Length) 
         { $Line -Replace '-','' } 
    else 
         { $Line } ; 
    $Line_number_were_on++ 
} 

I like one-liners, but I find that readability tends to suffer sometimes when I put terseness over function. If what you're doing is going to be part of a script that other people will be reading/maintaining, readability might be something to consider.

Carlos Nunez
  • 2,047
  • 1
  • 18
  • 20
  • There are a few issues with this script. First, it only does work on the last line, not first and last. Second, `$line` is never put back into anything, so the work is done and then lost, ever put back into the file. – Nick Jan 18 '13 at 21:16
0

Following Nick's answer: I do need to do this on all text files in the directory tree and this is what I'm using now:

Get-ChildItem -Path "c:\work\test" -Filter *.i | where { !$_.PSIsContainer } | % { 
$txt = Get-Content $_.FullName; 
$txt[0] = $txt[0] -replace '-'; 
$txt[$txt.length - 1 ] = $txt[$txt.length - 1 ] -replace '-';
$txt | Set-Content $_.FullName
}

and it looks like it's working well now.

animuson
  • 53,861
  • 28
  • 137
  • 147
Blitzcrank
  • 917
  • 2
  • 10
  • 19
0

I was recently searching for comments in the last line of .bat files. It seems to mess up the error code of previous commands. I found this useful for searching for a pattern in the last line of files. Pspath is a hidden property that get-content outputs. If I used select-string, I would lose the filename. *.bat gets passed as -filter for speed.

get-childitem -recurse . *.bat | get-content -tail 1 | where { $_ -match 'rem' } | 
  select pspath


PSPath
------
Microsoft.PowerShell.Core\FileSystem::C:\users\js\foo\file.bat

js2010
  • 23,033
  • 6
  • 64
  • 66
-1

Simple process: Replace $file.txt with your filename

Get-Content $file_txt | Select-Object -last 1