Getting BCD entries with .NET (PowerShell or .NET) - c#

I'm creating an application that analyzes the entries in the Boot Configuration Data (BCD).
I've tried with PowerShell, but it seems that it doesn't provide any cmdlets to deal with it. So, I've fallen back to .NET, espically C#.
I would like to have something obtain the BCD entries like this
var entries = bcd.GetEntries();
with entries being an IList<BcdEntry>
class BcdEntry
{
public string Name {get; set; }
IDictionary<string, IList<string>> Properties { get; set; }
}
The problem is that I don't know how to obtain the entries. Invoking BCDEdit is a possibility, but it requires to parse the output of the command, that is a tedious task.
I hope you can think of a solution for my problem.

A PSv4+ solution that parses bcdedit.exe /enum output into a list of custom objects:
# IMPORTANT: bcdedit /enum requires an ELEVATED session.
$bcdOutput = (bcdedit /enum) -join "`n" # collect bcdedit's output as a *single* string
# Initialize the output list.
$entries = New-Object System.Collections.Generic.List[pscustomobject]]
# Parse bcdedit's output.
($bcdOutput -split '(?m)^(.+\n-)-+\n' -ne '').ForEach({
if ($_.EndsWith("`n-")) { # entry header
$entries.Add([pscustomobject] #{ Name = ($_ -split '\n')[0]; Properties = [ordered] #{} })
} else { # block of property-value lines
($_ -split '\n' -ne '').ForEach({
$propAndVal = $_ -split '\s+', 2 # split line into property name and value
if ($propAndVal[0] -ne '') { # [start of] new property; initialize list of values
$currProp = $propAndVal[0]
$entries[-1].Properties[$currProp] = New-Object Collections.Generic.List[string]
}
$entries[-1].Properties[$currProp].Add($propAndVal[1]) # add the value
})
}
})
# Output a quick visualization of the resulting list via Format-Custom
$entries | Format-Custom
Note:
As LotPing observes,
bcdedit.exe output is partially localized; specifically, the following items:
entry headers (e.g., English Windows Boot Manager is Administrador de arranque de Windows in Spanish)
curiously, also the name of the property named identifier in English (e.g., Identificador in Spanish).
For the sake of brevity, the code makes no attempt to map localized names to their US-English counterparts, but it could be done.
Also, the sample bcdedit output posted with this ServerFault question (a duplicate) suggests that there may be property names that are so long that they run into their values, without intervening whitespace and without truncation.
If that is not just an artifact of posting, more work would be needed to handle this case; this article contains a list of property names.
[pscustomobject] instances are used rather than instances of a custom BcdEntry class; in PSv5+, you could create such a custom class directly in PowerShell.
The property values are all captured as string values, collected in a [List[string]] list (even if there's only 1 value); additional work would be required to interpret them as specific types;
e.g., [int] $entries[1].Properties['allowedinmemorysettings'][0] to convert string '0x15000075' to an integer.
Sample input / output:
Given bcdedit.exe /enum output such as this...
Windows Boot Manager
--------------------
identifier {bootmgr}
device partition=C:
displayorder {current}
{e37fc869-68b0-11e8-b4cf-806e6f6e6963}
description Windows Boot Manager
locale en-US
inherit {globalsettings}
default {current}
resumeobject {9f3d8468-592f-11e8-a07d-e91e7e2fad8b}
toolsdisplayorder {memdiag}
timeout 0
Windows Boot Loader
-------------------
identifier {current}
device partition=C:
path \WINDOWS\system32\winload.exe
description Windows 10
locale en-US
inherit {bootloadersettings}
recoverysequence {53f531de-590e-11e8-b758-8854872f7fe5}
displaymessageoverride Recovery
recoveryenabled Yes
allowedinmemorysettings 0x15000075
osdevice partition=C:
systemroot \WINDOWS
resumeobject {9f3d8468-592f-11e8-a07d-e91e7e2fad8b}
nx OptIn
bootmenupolicy Standard
... the above command yields this:
class PSCustomObject
{
Name = Windows Boot Manager
Properties =
[
class DictionaryEntry
{
Key = identifier
Value =
[
{bootmgr}
]
Name = identifier
}
class DictionaryEntry
{
Key = device
Value =
[
partition=C:
]
Name = device
}
class DictionaryEntry
{
Key = displayorder
Value =
[
{current}
{e37fc869-68b0-11e8-b4cf-806e6f6e6963}
]
Name = displayorder
}
class DictionaryEntry
{
Key = description
Value =
[
Windows Boot Manager
]
Name = description
}
...
]
}
class PSCustomObject
{
Name = Windows Boot Loader
Properties =
[
class DictionaryEntry
{
Key = identifier
Value =
[
{current}
]
Name = identifier
}
class DictionaryEntry
{
Key = device
Value =
[
partition=C:
]
Name = device
}
class DictionaryEntry
{
Key = path
Value =
[
\WINDOWS\system32\winload.exe
]
Name = path
}
class DictionaryEntry
{
Key = description
Value =
[
Windows 10
]
Name = description
}
...
]
}
To process the entries programmatically:
foreach($entry in $entries) {
# Get the name.
$name = $entry.Name
# Get a specific property's value.
$prop = 'device'
$val = $entry.Properties[$prop] # $val is a *list*; e.g., use $val[0] to get the 1st item
}
Note: $entries | ForEach-Object { <# work with entry $_ #> }, i.e. using the pipeline is an option too, but if the list of entries is already in memory, a foreach loop is faster.

I made some changes to #mklement0 script, too much to put in comments.
To solve the multiline properties problem these properties (which all
seem to be enclosed in curly braces) are joined with a RegEx replace.
to be locale independent the script uses just the dash line marking
the section header, to split contents (one caveat it inserts a blank
first entry)
I was wondering why there were only 4 Dictionary entries in the
output until I found the default value for $FormatEnumerationLimit
is 4
To avoid line breaks in output the script uses Out-String -Width 4096
## Q:\Test\2018\06\20\SO_50946956.ps1
# IMPORTANT: bcdedit /enu, requires an ELEVATED session.
#requires -RunAsAdministrator
## the following line imports the file posted by SupenJMN for testing
$bcdOutput = (gc ".\BCDEdit_ES.txt") -join "`n" -replace '\}\n\s+\{','},{'
## for a live "bcdedit /enum all" uncomment the following line
# $bcdOutput = (bcdedit /enum all) -join "`n" -replace '\}\n\s+\{','},{'
# Create the output list.
$entries = New-Object System.Collections.Generic.List[pscustomobject]]
# Parse bcdedit's output into entry blocks and construct a hashtable of
# property-value pairs for each.
($bcdOutput -split '(?m)^([a-z].+)\n-{10,100}\n').ForEach({
if ($_ -notmatch ' +') {
$entries.Add([pscustomobject] #{ Name = $_; Properties = [ordered] #{} })
} else {
($_ -split '\n' -ne '').ForEach({
$keyValue = $_ -split '\s+', 2
$entries[-1].Properties[$keyValue[0]] = $keyValue[1]
})
}
})
# Output a quick visualization of the resulting list via Format-Custom
$FormatEnumerationLimit = 20
$entries | Format-Custom | Out-String -Width 4096 | Set-Content BCDEdit_ES_Prop.txt
Shorted sample output of the script (~700 lines)
class PSCustomObject
{
Name =
Properties =
[
]
}
class PSCustomObject
{
Name = Administrador de arranque de firmware
Properties =
[
class DictionaryEntry
{
Key = Identificador
Value = {fwbootmgr}
Name = Identificador
}
class DictionaryEntry
{
Key = displayorder
Value = {bootmgr},{e37fc869-68b0-11e8-b4cf-806e6f6e6963},{05d4f193-712c-11e8-b4ea-806e6f6e6963},{05d4f194-712c-11e8-b4ea-806e6f6e6963},{cb6d5609-712f-11e8-b4eb-806e6f6e6963},{cb6d560a-712f-11e8-b4eb-806e6f6e6963},{cb6d560b-712f-11e8-b4eb-806e6f6e6963}
Name = displayorder
}
class DictionaryEntry
{
Key = timeout
Value = 1
Name = timeout
}
]
}

My approach would look somewhat like this:
(bcdedit /enum | Out-String) -split '(?<=\r\n)\r\n' | ForEach-Object {
$name, $data = $_ -split '\r\n---+\r\n'
$props = [ordered]#{
'name' = $name.Trim()
}
$data | Select-String '(?m)^(\S+)\s\s+(.*)' -AllMatches |
Select-Object -Expand Matches |
ForEach-Object { $props[$_.Groups[1].Value] = $_.Groups[2].Value.Trim() }
[PSCustomObject]$props
}
The above code basically starts with merging the bcdedit output into a single string like the other answers do, then splits that string into blocks of boot configuration data. Each of these blocks is then split again to separate the title from the actual data. The title is added to a hashtable as the name of the boot config section, then the data block is parsed with a regular expression for key/value pairs. These are appended to the hashtable, which is finally converted to a custom object.
Because of the the ordered and PSCustomObject type accelerators the code requires at least PowerShell v3.
Of course there are various optimizations you could apply to the basic example code above. For instance, different boot config sections might have different properties. The boot manager section has properties like toolsdisplayorder and timeout that are not present in the boot loader section, and the boot loader section has properties like osdevice and systemroot that are not present in the boot manager section. If you want a consistent set of properties for all generated objects you could pipe them through a Select-Object with a list of the properties you want your objects to have, e.g.:
... | Select-Object 'name', 'identifier', 'default', 'osdevice' 'systemroot'
Properties not present in the list will be dropped from the objects, while properties that are not present in an object will be added with an empty value.
Also, instead of creating all values as strings you could convert them to a more fitting type or just modify the value, e.g. to remove curly brackets from a string.
... | ForEach-Object {
$key = $_.Groups[1].Value
$val = $_.Groups[2].Value.Trim()
$val = $val -replace '^\{(.*)\}$', '$1'
if ($val -match '^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$') {
$val = [guid]$val
} elseif ($val -eq 'yes' -or $val -eq 'true') {
$val = $true
} elseif ($val -eq 'no' -or $val -eq 'false') {
$val = $false
} elseif ($key -eq 'locale') {
$val = [Globalization.CultureInfo]$val
}
$props[$key] = $val
}

Related

Powershell to rename particular excel sheet

I need to quickly rename particular excel sheet. The xlsx file itself has many of them (dates - I have to point out the newest by name change). The only thing I found is the ability to change the name of the first worksheet. Any hints guys? I'm a total layman when it comes to c#
$xlspath = "D:\New folder\Testing.xlsx"
$xldoc = new-object -comobject Excel.application
$workbook = $xldoc.Workbooks.Open($xlspath )
$worksheet = $workbook.worksheets.item(1)
$worksheet.name = "Result"
$worksheet.SaveAS = ($xlspath)
$worksheet.Close()
$xldoc.Quit()
Without knowing the date format you have used to name the worksheets, below code should do what you want:
$xlspath = "D:\Test\Testing.xlsx"
$xldoc = New-Object -ComObject Excel.Application
$xldoc.Visible = $false
$xldoc.DisplayAlerts = $false
$workbook = $xldoc.Workbooks.Open($xlspath )
# find the worksheet that is named for the latest date
$latestSheet = ($workbook.WorkSheets |
Sort-Object #{Expression = { (Get-Date $_.Name) }} |
Select-Object -Last 1).Name
# get the worksheet object by its name
$worksheet = $workbook.WorkSheets.Item($latestSheet)
# and rename it
$worksheet.Name = "Result"
# close and save
$workbook.Close($true) # $true means 'save the changes'
$xldoc.Quit()
# Important! release the COM objects from memory
$null = [System.Runtime.Interopservices.Marshal]::ReleaseComObject($worksheet)
$null = [System.Runtime.Interopservices.Marshal]::ReleaseComObject($workbook)
$null = [System.Runtime.Interopservices.Marshal]::ReleaseComObject($xldoc)
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()
Before:
After:
As you can see my date format is Dutch (NL), so in the form of dd-MM-yyyy. Yours could be different, so you may need to change Get-Date $_.Name into [datetime]$_.Name
Edit
If you have more excel files like that in a folder, you can do this:
$xlspath = "D:\Test"
$xldoc = New-Object -ComObject Excel.Application
$xldoc.Visible = $false
$xldoc.DisplayAlerts = $false
# get the files and iterate over them
Get-ChildItem -Path $xlspath -Filter '*.xlsx' -File | ForEach-Object {
$workbook = $xldoc.Workbooks.Open($_.FullName )
# test if there isn't already a worksheet named 'Result' in that file
try {
$worksheet = $workbook.WorkSheets.Item("Result")
Write-Warning "File '$($_.FullName)' already has a sheet called 'Result'. Skipping file."
$workbook.Close($false)
continue # skip this file and proceed with the next
}
catch {}
# find the worksheet that is named for the latest date
$latestSheet = ($workbook.WorkSheets |
Sort-Object #{Expression = { (Get-Date $_.Name) }} |
Select-Object -Last 1).Name
# get the worksheet object by its name
$worksheet = $workbook.WorkSheets.Item($latestSheet)
# and rename it
$worksheet.Name = "Result"
# close and save
$workbook.Close($true) # $true means 'save the changes'
}
$xldoc.Quit()
# Important! release the COM objects from memory
$null = [System.Runtime.Interopservices.Marshal]::ReleaseComObject($worksheet)
$null = [System.Runtime.Interopservices.Marshal]::ReleaseComObject($workbook)
$null = [System.Runtime.Interopservices.Marshal]::ReleaseComObject($xldoc)
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()

Shorter cast PsObject[long] to DateTime

I'm curious. Is it possible to cut this code?
using (PowerShell powerShell = PowerShell.Create())
{
powerShell.AddScript("Get-ADUser " + Login + " -Properties msDS-UserPasswordExpiryTimeComputed | Select -Expand \"msDS-UserPasswordExpiryTimeComputed\"");
Collection<PSObject> psObjects;
psObjects = powerShell.Invoke();
long PasswordExpireTemp = long.Parse(psObjects.FirstOrDefault().ToString());
userViewModel.PasswordExpire = DateTime.FromFileTimeUtc(PasswordExpireTemp);
}
I mean, skip creating long PasswordExpireTemp.
PsObject is Object[long.
userViewModel.PasswordExpire is DateTime?
sure, if you continue to do the work in PowerShell:
powerShell.AddScript("[datetime]::FromFileTimeUtc((Get-ADUser " + Login + " -Properties msDS-UserPasswordExpiryTimeComputed | Select -Expand \"msDS-UserPasswordExpiryTimeComputed\"")));
However, both bits of code will return a datetime of: Monday, January 1, 1601 12:00:00 AM; under the conditions where 'msDS-UserPasswordExpiryTimeComputed' is '0' or null:
If any of the ADS_UF_SMARTCARD_REQUIRED, ADS_UF_DONT_EXPIRE_PASSWD,
ADS_UF_WORKSTATION_TRUST_ACCOUNT, ADS_UF_SERVER_TRUST_ACCOUNT,
ADS_UF_INTERDOMAIN_TRUST_ACCOUNT bits is set in TO!userAccountControl,
then TO!msDS-UserPasswordExpiryTimeComputed = 0x7FFFFFFFFFFFFFFF.
Else, if TO!pwdLastSet = null, or TO!pwdLastSet = 0, then
TO!msDS-UserPasswordExpiryTimeComputed = 0.
Else, if Effective-MaximumPasswordAge = 0x8000000000000000, then
TO!msDS-UserPasswordExpiryTimeComputed = 0x7FFFFFFFFFFFFFFF (where
Effective-MaximumPasswordAge is defined in [MS-SAMR] section 3.1.1.5).
Else, TO!msDS-UserPasswordExpiryTimeComputed = TO!pwdLastSet +
Effective-MaximumPasswordAge (where Effective-MaximumPasswordAge is
defined in [MS-SAMR] section 3.1.1.5).
https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/f9e9b7e2-c7ac-4db6-ba38-71d9696981e9
Hi Hoshie, you're right, I was missing parenthesis wrapping the inner command in it's own. As far as possible errors, please see the edit below:
PS C:\Windows\system32> (Get-ADUser [MyUserAccount] -Properties msDS-UserPasswordExpiryTimeComputed | Select-Object -ExpandProperty msDS-UserPasswordExpiryTimeComputed)
9223372036854775807
PS C:\Windows\system32> [int64]::MaxValue 9223372036854775807
PS C:\Windows\system32> [datetime]::MaxValue
Friday, December 31, 9999 11:59:59 PM
PS C:\Windows\system32> [datetime]::MaxValue.ToFileTimeUtc()
2650467743999999999
PS C:\Windows\system32> [datetime]::MaxValue.ToFileTime()
2650467743999999999
So, the problem you're running into is that the value that is returned from this property can be LARGER than the maximum possible value accepted by the DateTime object--refer to the MSDN article for all of the conditions where this value returns the Int64.MaxValue of '9223372036854775807' (0x7FFFFFFFFFFFFFFF).
You have to handle this possibility either in PowerShell or C#. Here is a sample of how it might be solved in PowerShell:
$var = (Get-ADUser [MyUserName] -Properties msDS-UserPasswordExpiryTimeComputed | Select-Object -ExpandProperty msDS-UserPasswordExpiryTimeComputed)
if ($var -and $var -ne [int64]::MaxValue) {
## Do something with the value
} else {
## Value is either $null or -eq to the Max Value of a Signed 64-bit integer
}
## OR
if ($var -and $var -ge 0 -and $var -lt [datetime]::MaxValue.ToFileTimeUtc()) {
$true
## Do something
}

Squirrel Powershell file is not creating the Setup.exe

I have the following PowerShell script being run on my build server
Write-Host "Current Path $env:Agent_BuildDirectory"
Write-Host "Build Number $env:Build_BuildNumber"
$squirrel = "$env:BUILD_SOURCESDIRECTORY\packages\squirrel.windows.*\tools\Squirrel.exe"
$releaseDir = '.\Releases'
$nugetPackFile = ".\MyApp\MyApp.$env:Build_BuildNumber.nupkg"
Write-Host $squirrel
Write-Host $nugetPackFile
if((Test-Path $nugetPackFile) -and (Test-Path $squirrel)) {
$squirrelArg1 = '--releasify=' + $nugetPackFile
$squirrelArg2 = '--releaseDir=' + $releaseDir
& $squirrel $squirrelArg1 $squirrelArg2
}
It runs and it creates only a nupkg in the .\Releases folder. If I run the same --releasify command in the Visual Studio instance on my build server agent it creates all the setup.exe and Releases file. Why is this PowerShell script not working the same way the command being run in the NuGet PowerShell window in VS is?
I've not played with Squirrel, so this may not work; but too much code here to just submit as a comment...
Try this:
Write-Host "Current Path $env:Agent_BuildDirectory"
Write-Host "Build Number $env:Build_BuildNumber"
$squirrel = Get-Item (Join-Path $env:BUILD_SOURCESDIRECTORY "packages\squirrel.windows.*\tools\Squirrel.exe") | select -First 1 -Expand FullName
$releaseDir = '.\Releases'
$nugetPackFile = ".\MyApp\MyApp.$env:Build_BuildNumber.nupkg"
Write-Host $squirrel
Write-Host $nugetPackFile
if((Test-Path $nugetPackFile) -and (Test-Path $squirrel)) {
$squirrelArg1 = "--releasify=`"$nugetPackFile`""
$squirrelArg2 = "--releaseDir=`"$releaseDir`""
& $squirrel $squirrelArg1 $squirrelArg2
}
Getting Squirrel.exe Path
(Join-Path $env:BUILD_SOURCESDIRECTORY "packages\squirrel.windows.*\tools\Squirrel.exe")
- here I use Join-Path to avoid any issues around whether or not the value of $env:BUILD_SOURCESDIRECTORY ends in a backslash.
Get-Item - I put this before that path so that it will resolve the path to a valid path (i.e. working out any matches of the asterisk/wildcard).
| select -First 1 -Expand FullName I then add this to get the first path which matches the result, and to return the full file path to squirrel.exe
Passing Parameters
For the statements below, I added double quotes around the paths; sometimes this is required to clarify which argument they relate to; particularly if there are any spaces or special characters in the paths. I also switched from using + to putting the variable within double quotes as this makes it simpler to concatenate the quotes within the string. I used backticks on the quotes in the string to escape those characters.
$squirrelArg1 = "--releasify=`"$nugetPackFile`""
$squirrelArg2 = "--releaseDir=`"$releaseDir`""
Hope that helps, but sadly this is very much guesswork by me; sorry.
Update
Getting the latest version; assuming asterisk in the path packages\squirrel.windows.*\tools\Squirrel.exe represents the version number in the form: Major.Minor.Build.
$squirrel = Get-Item (Join-Path $env:BUILD_SOURCESDIRECTORY "packages\squirrel.windows.*\tools\Squirrel.exe") | %{
if ($_ -match '.*\\squirrel\.windows\.(?<Major>\d+)\.(?<Minor>\d+)\.(?<Build>\d+)\\tools\\Squirrel\.exe') {
(new-object -TypeName PSObject -Property $matches)
}
} | sort #{e={$_.Major};a=0}, #{e={$_.Minor};a=0}, #{e={$_.Build};a=0} | select -First 1 -ExpandProperty '0'
I found the answer here
Write-Host "Current Path $env:Agent_BuildDirectory"
Write-Host "Build Number $env:Build_BuildNumber"
$squirrel = Get-Item (Join-Path $env:BUILD_SOURCESDIRECTORY "packages\squirrel.windows.*\tools\Squirrel.exe") | %{
if ($_ -match '.*\\squirrel\.windows\.(?<Major>\d+)\.(?<Minor>\d+)\.(?<Build>\d+)\\tools\\Squirrel\.exe') {
(new-object -TypeName PSObject -Property $matches)
}
} | sort #{e={$_.Major};a=0}, #{e={$_.Minor};a=0}, #{e={$_.Build};a=0} | select -First 1 -ExpandProperty '0'
Set-Alias Squirrel $squirrel
$releaseDir = '.\Releases'
$nugetPackFile = ".\MyApp\MyApp.$env:Build_BuildNumber.nupkg"
Write-Host $squirrel
Write-Host $nugetPackFile
if((Test-Path $nugetPackFile) -and (Test-Path $squirrel)) {
Squirrel --releasify $nugetPackFile --releaseDir $releaseDir | Write-Output
}
Much thanks goes to #JohnLBevan for helping to fix up my powershell code.

c# regular expression

I have an output like -
Col.A Col.B Col.C Col.D
--------------------------------------------------------------
* 1 S60-01-GE-44T-AC SGFM115001195 7520051202 A
1 S60-PWR-AC APFM115101302 7520047802 A
1 S60-PWR-AC APFM115101245 7520047802 A
or
Col.A Col.B Col.C Col.D
--------------------------------------------------------------
* 0 S50-01-GE-48T-AC DL252040175 7590005605 B
0 S50-PWR-AC N/A N/A N/A
0 S50-FAN N/A N/A N/A
For these outputs the regular expression -
(?:\*)?\s+(?<unitno>\d+)\s+\S+-\d+-(?:GE|TE)?-?(?:\d+(?:F|T))-?(?:(?:AC)|V)?\s+(?<serial>\S+)\s+\S+\s+\S+\s+\n
works fine to capture Column A and Column B. But recently I got a new kind of output -
Col.A Col.B Col.C Col.D
---------------------------------------------------------
* 0 S4810-01-64F HADL120620060 7590009602 A
0 S4810-PWR-AC H6DL120620060 7590008502 A
0 S4810-FAN N/A N/A N/A
0 S4810-FAN N/A N/A N/A
As you can see the patterns "GE|TE" and the "AC|V" are missing from these outputs. How do I change my regular expression accordingly maintaining backward compatibility.
EDIT:
The output that you see comes in a complete string and due to some operational limits I cannot use any other concept other than regex here to get my desired values. I know using split would be ideal here but I cannot.
You are probably better off using String.Split() to break the column values out into sperate strings and then processing them, rather that using a huge un-readable regular expression.
foreach (string line in lines) {
string[] colunnValues = line.Split((char[])null, StringSplitOptions.RemoveEmptyEntries);
...
}
A regular expression seems not to be the right approach here. Use a positional approach
string s = "* 0 S4810-01-64F HADL120620060 7590009602 A";
bool withStar = s[0] == '*';
string nr = s.Substring(2, 2).Trim();
string colA = s.Substring(5, 18).TrimEnd();
string colB = s.Substring(24, 14).TrimEnd();
...
UPDATE
I you want (or must) stick to Regex, test for the spaces instead of the values. Of cause this works only if the values never include spaces.
string[] result = Regex.Split(s, "\s+");
Of cause you can also search for non-spaces \S instead of \s.
MatchCollection matches = Regex.Matches(s, "\S+");
or excluding the star
(?:\*)?[^*\s]+
your regular expression doesn't even need GE or TE. See that ? after (?:GE|TE)?
that means that the previous group or symbol is optional.
the same is true with the AC and V section
I would not use regular expressions to parse these reports.
Instead, treat them as fixed column width reports after the headers are stripped off.
I would do something like (this is typed cold as an example, not tested even for syntax):
// Leaving off all public/private/error detection stuff
class ColumnDef
{
string Name { set; get; }
int FirstCol { set; get; }
int LastCol { set; get; }
}
ColumnDef[] report = new ColumnDef[]
{
{ Name = "ColA",
FirstCol = 0,
LastCol = 2
},
/// ... and so on for each column
}
IDictionary<string, string> ParseDataLine(string line)
{
var dummy = new Dictionary<string, string>();
foreach (var c in report)
{
dummy[c.Name] = line.Substring(c.FirstCol, c.LastCol).Trim();
}
}
This is an example of a generic ETL (Extract, Transform, and Load) problem--specifically the Extract stage.
You will have to strip out header and footer lines before using ParseDataLine, and I am not sure there is enough information shown to do that. Based on what your post says, any line that is blank, or doesn't start with a space or a * is a header/footer line to be ignored.
Why not try something like this (?:\*)?\s+(?<unitno>\d+)\s+\S+\s+(?<serial>\S+)\s+\S+\s+\S+(?:\s+)?\n
This is built off your provided regular expression and due to the trailing \n the provided input will need to end with a carriage return.

Playing with files that contain more than one consecutive space in their filenames

In some part of my code, I check if a file exists and then I open it.
One employee encountered a problem with filenames containing more than one space character.
I checked and it's true. Here's a snippet of my code:
string filePath = Path.Combine(helper.MillTestReportPath, fileName);
// Ouverture du fichier
if (File.Exists(filePath))
{
Process.Start(filePath);
}
else
{
MessageBox.Show("Le fichier n'existe pas!", "Fichier introuvable", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
Everything works just find with almost every file but when a file ("SPAM CERTS S O 94318099 P O 10610.msg" for example) contains more than one space, I get false with File.Exists and even if I directly try to run Process.Start it fails...
Any idea about how I could fix that?
Thanks a lot!
According to MSDN documentation, File.Exists returns:
true if the caller has the required permissions and path contains the name of an existing file; otherwise, false. This method also returns false if path is null, an invalid path, or a zero-length string. If the caller does not have sufficient permissions to read the specified file, no exception is thrown and the method returns false regardless of the existence of path.
If the file exists, then probably the user that is trying to access the file does not have necessary permissions.
I suspect your filename(s) do not only contain ANSI space characters (char)32 0020hex but other ANSI characters that are indistinguishable from space characters.
If your files reside on an NTFS drive, file names can even contain Unicode characters.
MSDN: Character Sets Used in File Names
I wrote a small PowerShell script that shows you the filenames of the current folder in hex
dir | % {
$chars = $_.Name.ToCharArray(); """$($_.Name)""";
$result = "|";
foreach ($char in $chars) {
$result += [String]::Format(" {0} |",$char)
};
"$result";
$result = "|"
foreach ($char in $chars) {
$hexChar = [System.Convert]::ToInt32($char);
$result += $hexChar.ToString("x4");
$result += "|";
};
"$result`r`n";
}
Typical output is
"1000 €.txt"
| 1 | 0 | 0 | 0 | | € | . | t | x | t |
|0031|0030|0030|0030|0020|20ac|002e|0074|0078|0074|
"A normal file.txt"
| A | | n | o | r | m | a | l | | f | i | l | e | . | t | x | t |
|0041|0020|006e|006f|0072|006d|0061|006c|0020|0066|0069|006c|0065|002e|0074|0078|0074|
"what the ңёςк.txt"
| w | h | a | t | | t | h | e | | ң | ё | ς | к | . | t | x | t |
|0077|0068|0061|0074|0020|0074|0068|0065|0020|04a3|0451|03c2|043a|002e|0074|0078|0074|
etc.
You can see real ANSI spaces as 0020hex here.
The number of spaces should not be a problem. Did you check the output string of filePath? I'm sure it will not be right. As Henk suggested, if the output is not correct try to change to Path.Combine().
Bonne journée
Ran this test code using the same filename you've specified:
const string path = #"C:\TEMP\SPAM CERTS S O 94318099 P O 10610.msg";
if (File.Exists(path)) {
Trace.WriteLine("EXIST");
Process.Start(path);
}
else {
Trace.WriteLine("NOT EXIST");
}
The file is correctly found to exist even with multiple sequential spaces, etc. It also successfully launches the associated program (Noteapad++ in my case).
I suspect, as others indicate, that your problem is elsewhere. What is the failure you are seeing with Process.Start?
What happens when you try this with that file path:
try {
string path = Path.Combine(helper.MillTestReportPath, fileName);
using (FileStream fs = File.Open(path, FileMode.CreateNew)) {
}
} catch (IOException ex) {
// any exception here?
}
I think others have already covered the File related stuff I would check but have you considered localization/encoding in your check string vs the local file system?
It appears you are using ?German? in your message box prompt, might be comparing apples to oranges... just a thought.
Hey. Please list the parent dir programmatically. And then for each child file echo all the filenames char-by-char. Preferrably in hex or unicode codes.
You're probably having some non-trivial whitespace character somewhere in the file name. Especially if the filename was generated automatically from some keystore or xml file.

Categories