diff --git a/.gitignore b/.gitignore index e97d30b..199848a 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ project.lock.json Microsoft.PowerShell.TextUtility.xml # VSCode directories that are not at the repository root /**/.vscode/ +# Visual Studio IDE directory +.vs/ \ No newline at end of file diff --git a/README.md b/README.md index 3ddb4aa..62734e7 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,10 @@ Return a base64 encoded representation of a string. This will convert tabular data and convert it to an object. +## Get-FileEncoding + +This cmdlet returns encoding for a file. + ## Code of Conduct Please see our [Code of Conduct](.github/CODE_OF_CONDUCT.md) before participating in this project. diff --git a/TextUtility.sln b/TextUtility.sln new file mode 100644 index 0000000..29764b9 --- /dev/null +++ b/TextUtility.sln @@ -0,0 +1,30 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{CBF81F7C-6E0A-4695-AA0F-35C61676B7EB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.PowerShell.TextUtility", "src\code\Microsoft.PowerShell.TextUtility.csproj", "{B5249E6A-DBD6-49AD-B78B-DFF09CB9D26F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B5249E6A-DBD6-49AD-B78B-DFF09CB9D26F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B5249E6A-DBD6-49AD-B78B-DFF09CB9D26F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B5249E6A-DBD6-49AD-B78B-DFF09CB9D26F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B5249E6A-DBD6-49AD-B78B-DFF09CB9D26F}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {B5249E6A-DBD6-49AD-B78B-DFF09CB9D26F} = {CBF81F7C-6E0A-4695-AA0F-35C61676B7EB} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {EAC68D5E-4DA0-4F37-88B6-CAF32652C01F} + EndGlobalSection +EndGlobal diff --git a/src/Microsoft.PowerShell.TextUtility.psd1 b/src/Microsoft.PowerShell.TextUtility.psd1 index ddb5209..7e2372c 100644 --- a/src/Microsoft.PowerShell.TextUtility.psd1 +++ b/src/Microsoft.PowerShell.TextUtility.psd1 @@ -13,7 +13,7 @@ PowerShellVersion = '5.1' FormatsToProcess = @('Microsoft.PowerShell.TextUtility.format.ps1xml') CmdletsToExport = @( - 'Compare-Text','ConvertFrom-Base64','ConvertTo-Base64',"ConvertFrom-TextTable" + 'Compare-Text','ConvertFrom-Base64','ConvertTo-Base64',"ConvertFrom-TextTable","Get-FileEncoding" ) PrivateData = @{ PSData = @{ diff --git a/src/code/GetFileEncodingCommand.cs b/src/code/GetFileEncodingCommand.cs new file mode 100644 index 0000000..5fee905 --- /dev/null +++ b/src/code/GetFileEncodingCommand.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.IO; +using System.Management.Automation; +using System.Text; + +namespace Microsoft.PowerShell.TextUtility +{ + /// + /// This class implements the Get-FileEncoding command. + /// + [Cmdlet(VerbsCommon.Get, "FileEncoding", DefaultParameterSetName = PathParameterSet)] + [OutputType(typeof(Encoding))] + public sealed class GetFileEncodingCommand : PSCmdlet + { + #region Parameter Sets + + private const string PathParameterSet = "ByPath"; + private const string LiteralPathParameterSet = "ByLiteralPath"; + + #endregion + + #region Parameters + + /// + /// Gets or sets path from from which to get encoding. + /// + [Parameter(Position = 0, Mandatory = true, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = PathParameterSet)] + public string Path { get; set; } + + /// + /// Gets or sets literal path from which to get encoding. + /// + [Parameter(Position = 0, Mandatory = true, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = LiteralPathParameterSet)] + [Alias("PSPath", "LP")] + public string LiteralPath + { + get + { + return _isLiteralPath ? Path : null; + } + + set + { + Path = value; + _isLiteralPath = true; + } + } + + private bool _isLiteralPath; + + #endregion + + /// + /// Process paths to get file encoding. + /// + protected override void ProcessRecord() + { + string resolvedPath = PathUtils.ResolveFilePath(Path, this, _isLiteralPath); + + if (!File.Exists(resolvedPath)) + { + PathUtils.ReportPathNotFound(Path, this); + } + + WriteObject(PathUtils.GetPathEncoding(resolvedPath)); + } + } +} \ No newline at end of file diff --git a/src/code/Microsoft.PowerShell.TextUtility.csproj b/src/code/Microsoft.PowerShell.TextUtility.csproj index ad59249..9102fe6 100644 --- a/src/code/Microsoft.PowerShell.TextUtility.csproj +++ b/src/code/Microsoft.PowerShell.TextUtility.csproj @@ -17,7 +17,7 @@ all - + all @@ -28,4 +28,19 @@ + + + True + True + PathUtilityStrings.resx + + + + + + ResXFileCodeGenerator + PathUtilityStrings.Designer.cs + + + diff --git a/src/code/PathUtils.cs b/src/code/PathUtils.cs new file mode 100644 index 0000000..3e91b40 --- /dev/null +++ b/src/code/PathUtils.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.ObjectModel; +using System.Globalization; +using System.IO; +using System.Management.Automation; +using System.Text; +using Microsoft.PowerShell.TextUtility.Resources; + +namespace Microsoft.PowerShell.TextUtility +{ + /// + /// Defines generic path utilities and helper methods for TextUtility. + /// + internal static class PathUtils + { + /// + /// Resolves user provided path using file system provider. + /// + /// The path to resolve. + /// The command. + /// True if the wildcard resolution should not be attempted. + /// The resolved (absolute) path. + internal static string ResolveFilePath(string path, PSCmdlet command, bool isLiteralPath) + { + string resolvedPath; + + try + { + ProviderInfo provider = null; + PSDriveInfo drive = null; + + PathIntrinsics sessionStatePath = command.SessionState.Path; + + if (isLiteralPath) + { + resolvedPath = sessionStatePath.GetUnresolvedProviderPathFromPSPath(path, out provider, out drive); + } + else + { + Collection filePaths = sessionStatePath.GetResolvedProviderPathFromPSPath(path, out provider); + + if (!provider.Name.Equals("FileSystem", StringComparison.OrdinalIgnoreCase)) + { + ReportOnlySupportsFileSystemPaths(path, command); + } + + if (filePaths.Count > 1) + { + ReportMultipleFilesNotSupported(command); + } + + resolvedPath = filePaths[0]; + } + } + catch (ItemNotFoundException) + { + resolvedPath = null; + } + + return resolvedPath; + } + + /// + /// Throws terminating error for not using file system provider. + /// + /// The path to report. + /// The command. + internal static void ReportOnlySupportsFileSystemPaths(string path, PSCmdlet command) + { + var errorMessage = string.Format(CultureInfo.CurrentCulture, PathUtilityStrings.OnlySupportsFileSystemPaths, path); + var exception = new ArgumentException(errorMessage); + var errorRecord = new ErrorRecord(exception, "OnlySupportsFileSystemPaths", ErrorCategory.InvalidArgument, path); + command.ThrowTerminatingError(errorRecord); + } + + /// + /// Throws terminating error for path not found. + /// + /// The path to report. + /// The command. + internal static void ReportPathNotFound(string path, PSCmdlet command) + { + var errorMessage = string.Format(CultureInfo.CurrentCulture, PathUtilityStrings.PathNotFound, path); + var exception = new ArgumentException(errorMessage); + var errorRecord = new ErrorRecord(exception, "PathNotFound", ErrorCategory.ObjectNotFound, path); + command.ThrowTerminatingError(errorRecord); + } + + /// + /// Throws terminating error for multiple files being used. + /// + /// The command. + internal static void ReportMultipleFilesNotSupported(PSCmdlet command) + { + var errorMessage = string.Format(CultureInfo.CurrentCulture, PathUtilityStrings.MultipleFilesNotSupported); + var exception = new ArgumentException(errorMessage); + var errorRecord = new ErrorRecord(exception, "MultipleFilesNotSupported", ErrorCategory.InvalidArgument, null); + command.ThrowTerminatingError(errorRecord); + } + + /// + /// Gets encoding for path. + /// + /// The path to get file encoding. + /// The encoding of file. + internal static Encoding GetPathEncoding(string path) + { + using (var reader = new StreamReader(path, Encoding.Default, detectEncodingFromByteOrderMarks: true)) + { + _ = reader.Read(); + return reader.CurrentEncoding; + } + } + } +} \ No newline at end of file diff --git a/src/code/Resources/PathUtilityStrings.Designer.cs b/src/code/Resources/PathUtilityStrings.Designer.cs new file mode 100644 index 0000000..5974d58 --- /dev/null +++ b/src/code/Resources/PathUtilityStrings.Designer.cs @@ -0,0 +1,105 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.PowerShell.TextUtility.Resources +{ + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class PathUtilityStrings + { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal PathUtilityStrings() + { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager + { + get + { + if (object.ReferenceEquals(resourceMan, null)) + { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.PowerShell.TextUtility.Resources.PathUtilityStrings", typeof(PathUtilityStrings).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture + { + get + { + return resourceCulture; + } + set + { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Cannot perform operation because the path resolved to more than one file. This command cannot operate on multiple files.. + /// + internal static string MultipleFilesNotSupported + { + get + { + return ResourceManager.GetString("MultipleFilesNotSupported", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The given path '{0}' is not supported. This command only supports the FileSystem Provider paths.. + /// + internal static string OnlySupportsFileSystemPaths + { + get + { + return ResourceManager.GetString("OnlySupportsFileSystemPaths", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cannot find path '{0}' because it does not exist.. + /// + internal static string PathNotFound + { + get + { + return ResourceManager.GetString("PathNotFound", resourceCulture); + } + } + } +} diff --git a/src/code/Resources/PathUtilityStrings.resx b/src/code/Resources/PathUtilityStrings.resx new file mode 100644 index 0000000..23ee479 --- /dev/null +++ b/src/code/Resources/PathUtilityStrings.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Cannot perform operation because the path resolved to more than one file. This command cannot operate on multiple files. + + + The given path '{0}' is not supported. This command only supports the FileSystem Provider paths. + + + Cannot find path '{0}' because it does not exist. + + \ No newline at end of file diff --git a/test/Get-FileEncoding.Tests.ps1 b/test/Get-FileEncoding.Tests.ps1 new file mode 100644 index 0000000..10c2a21 --- /dev/null +++ b/test/Get-FileEncoding.Tests.ps1 @@ -0,0 +1,52 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Describe "Get-FileEncoding" -Tags "CI" { + BeforeAll { + $testFilePath = Join-Path -Path $TestDrive -ChildPath test.txt + $testFileLiteralPath = Join-Path -Path $TestDrive -ChildPath "[test].txt" + $content = 'abc' + + $testCases = @( + @{ EncodingName = 'ascii'; ExpectedEncoding = 'utf-8' } + @{ EncodingName = 'bigendianunicode'; ExpectedEncoding = 'utf-16BE' } + @{ EncodingName = 'bigendianutf32'; ExpectedEncoding = 'utf-32BE' } + @{ EncodingName = 'oem'; ExpectedEncoding = 'utf-8' } + @{ EncodingName = 'unicode'; ExpectedEncoding = 'utf-16' } + @{ EncodingName = 'utf8'; ExpectedEncoding = 'utf-8' } + @{ EncodingName = 'utf8BOM'; ExpectedEncoding = 'utf-8' } + @{ EncodingName = 'utf8NoBOM'; ExpectedEncoding = 'utf-8' } + @{ EncodingName = 'utf32'; ExpectedEncoding = 'utf-32' } + ) + } + + It "Validate Get-FileEncoding using -Path returns file encoding for ''" -TestCases $testCases { + param($EncodingName, $ExpectedEncoding) + Set-Content -Path $testFilePath -Encoding $EncodingName -Value $content -Force + (Get-FileEncoding -Path $testFilePath).BodyName | Should -Be $ExpectedEncoding + (Get-ChildItem -Path $testFilePath | Get-FileEncoding).BodyName | Should -Be $ExpectedEncoding + } + + It "Validate Get-FileEncoding using -LiteralPath returns file encoding for ''" -TestCases $testCases { + param($EncodingName, $ExpectedEncoding) + Set-Content -LiteralPath $testFileLiteralPath -Encoding $EncodingName -Value $content -Force + (Get-FileEncoding -LiteralPath $testFileLiteralPath).BodyName | Should -Be $ExpectedEncoding + (Get-ChildItem -LiteralPath $testFileLiteralPath | Get-FileEncoding).BodyName | Should -Be $ExpectedEncoding + } + + It "Should throw exception if path is not found using -Path" { + { Get-FileEncoding -Path nonexistentpath } | Should -Throw -ErrorId 'PathNotFound,Microsoft.PowerShell.TextUtility.GetFileEncodingCommand' + } + + It "Should throw exception if path is not found using -LiteralPath" { + { Get-FileEncoding -LiteralPath nonexistentpath } | Should -Throw -ErrorId 'PathNotFound,Microsoft.PowerShell.TextUtility.GetFileEncodingCommand' + } + + It "Should throw exception if path is not file system path" { + { Get-FileEncoding -Path 'Env:' } | Should -Throw -ErrorId 'OnlySupportsFileSystemPaths,Microsoft.PowerShell.TextUtility.GetFileEncodingCommand' + } + + It "Should throw exception if multiple paths is specified" { + { Get-FileEncoding -Path '*' } | Should -Throw -ErrorId 'MultipleFilesNotSupported,Microsoft.PowerShell.TextUtility.GetFileEncodingCommand' + } +} \ No newline at end of file