diff --git a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs index af720fc..4ee08ef 100644 --- a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs +++ b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs @@ -3,6 +3,7 @@ namespace UniqueFileGenerator.Console open Errors open Utilities open System +open System.IO open FSharpPlus open CCFSharpUtils.Text @@ -18,6 +19,15 @@ module ArgTypes = |> tryParseInRange (floor, ceiling) |> Result.mapError (fun _ -> NumberParseFailure (text, (floor, ceiling))) + /// Determines whether a string contains characters invalid for filnames on this OS. + let validateChars text = + let invalidChars = Path.GetInvalidFileNameChars() |> Set.ofArray + let isInvalidChar ch = Set.contains ch invalidChars + let rejectedChars = String.filter isInvalidChar text + if String.hasNoText rejectedChars + then Ok text + else Error (rejectedChars |> String.toList |> InvalidChars) + type FileCount = private FileCount of int with static member val AllowedRange = 1, Int32.MaxValue @@ -36,8 +46,8 @@ module ArgTypes = static member Create maybeText = maybeText - |> option id Prefix.Default - |> Prefix + |> option validateChars (Ok Prefix.Default) + |> map Prefix member this.Value = let (Prefix prefix) = this in prefix @@ -50,7 +60,7 @@ module ArgTypes = |> option (stripSeparatorsAndTrim >> tryParseInRange NameBaseLength.AllowedRange) (Ok NameBaseLength.Default) - |> Result.map NameBaseLength + |> map NameBaseLength member this.Value = let (NameBaseLength length) = this in length @@ -59,8 +69,8 @@ module ArgTypes = static member Create maybeText = maybeText - |> option String.trim Extension.Default - |> Extension + |> option (validateChars >> map String.trim) (Ok Extension.Default) + |> map Extension member this.Value = let (Extension ext) = this in ext diff --git a/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs b/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs index 7a7cd9b..64fa284 100644 --- a/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs +++ b/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs @@ -55,9 +55,9 @@ module ArgValidation = let! optionMap = toPairs (Array.tail args) let tryGetArg x = Map.tryFind flags[x] optionMap - let p = Prefix.Create (tryGetArg Prefix) + let! p = Prefix.Create (tryGetArg Prefix) let! b = NameBaseLength.TryCreate (tryGetArg NameBaseLength) - let e = Extension.Create (tryGetArg Extension) + let! e = Extension.Create (tryGetArg Extension) let o = OutputDirectory.Create (tryGetArg OutputDirectory) let! s = Size.TryCreate (tryGetArg Size) let! d = Delay.TryCreate (tryGetArg Delay) diff --git a/src/UniqueFileGenerator.Console/Errors.fs b/src/UniqueFileGenerator.Console/Errors.fs index fcdb43a..01ca52a 100644 --- a/src/UniqueFileGenerator.Console/Errors.fs +++ b/src/UniqueFileGenerator.Console/Errors.fs @@ -1,6 +1,8 @@ namespace UniqueFileGenerator.Console open Utilities +open CCFSharpUtils.Text +open FSharpPlus module Errors = @@ -11,6 +13,7 @@ module Errors = | UnknownFlags | DuplicateFlags | NumberParseFailure of Input: string * AllowedRange: (int * int) + | InvalidChars of Chars: char list | DirectoryMissing of string | DriveSpaceConfirmationFailure | DriveSpaceInsufficient of Needed: string * Actual: string @@ -24,7 +27,12 @@ module Errors = | UnknownFlags -> "Unknown option flag(s) found." | DuplicateFlags -> "Duplicate option flag(s) found. Each can only be used once." | NumberParseFailure (input, (floor, ceiling)) -> - $"The number \"%s{input}\" is out of bounds. Enter an integer between %s{Num.Format floor} and %s{Num.Format ceiling}, inclusive." + sprintf "\"%s\" is an invalid value. Enter an integer between %s and %s, inclusive." + input (Num.Format floor) (Num.Format ceiling) + | InvalidChars chars -> + sprintf "Found %s invalid for file names on this system: \"%s\"." + (String.pluralizeSWithCount "character" chars.Length) // "1 character," "2 characters," etc. + (chars |> map string |> String.concat "\", \"") | DirectoryMissing dirName -> $"Directory \"%s{dirName}\" was not found." | DriveSpaceConfirmationFailure -> "Could not confirm available drive space." | DriveSpaceInsufficient (needed, actual) -> diff --git a/src/UniqueFileGenerator.Tests/ArgParserTests.fs b/src/UniqueFileGenerator.Tests/ArgParserTests.fs index 40deba9..2e02bfd 100644 --- a/src/UniqueFileGenerator.Tests/ArgParserTests.fs +++ b/src/UniqueFileGenerator.Tests/ArgParserTests.fs @@ -18,13 +18,19 @@ let validOptionValues = Delay, "5_000" ] let defaultOptions = - { Prefix = Prefix.Create None |> _.Value + { Prefix = Prefix.Create None + |> function + | Ok x -> x.Value + | Error e -> failwith $"Unexpected parse error: {e}" NameBaseLength = NameBaseLength.TryCreate None |> function | Ok x -> x.Value | Error e -> failwith $"Unexpected parse error: {e}" - Extension = Extension.Create None |> _.Value + Extension = Extension.Create None + |> function + | Ok x -> x.Value + | Error e -> failwith $"Unexpected parse error: {e}" OutputDirectory = OutputDirectory.Create None |> _.Value Size = Size.TryCreate None |> function @@ -379,4 +385,3 @@ module SupportedSeparators = let result = stripSeparatorsAndTrim text Assert.Equal("helloworld\ttestexample\nmore", result) -