/* view.c: Display a file if it fits on screen, otherwise use an external viewer. Jason Hood, 11 November, 1999. Public Domain. 021017: handle screen wrap and tabs; use a buffer instead of re-reading the file (which also allows NUL). v1.00, 10 to 15 October, 2003: fix problems with buffer overrun; use VIEW-ER environment variable to specify viewer; added Win32 and 16-bit DOS ports; use the viewer if redirected; recognise character 255 (dumb EOF) and backspace. v1.01, 9, 10 & 12 January, 2004: Win32: recognise screen lines, not buffer lines; handle pipe better (use PeekNamedPipe); use spawn & P_WAIT instead of exec (P_OVERLAY); quote spawned arguments containing spaces. v1.02, 12 June, 2004: Win32: poll the pipe for data (instead of sleeping); remove "Unable to peek pipe" message (just assume empty); prefix error messages with "view: ". v1.03, 28 to 31 July, 2004: Win32: handle the pipe separately; use the viewer if input is redirected and a file is given; return 2 for out of memory, 3 for unable to execute, 4 for pipe problems; recognise BEL; fix more buffer overflow problems (caused by CR & BS). v1.04, 5 October, 2005: Win32: work around XP console bug with backspace; allow VIEW-ER to have an argument. v2.00, 27 & 28 October, 2014: Win32-specific version: write to a separate screen buffer to handle multibyte characters; wide file names. v2.01, 31 October, 2014: fix false wrap detection; don't write BEL to the screen buffer. */ #define PVERS "2.01" #define PDATE "31 October, 2014" #ifndef UNICODE #define UNICODE #endif #define _CRT_SECURE_NO_WARNINGS #define WIN32_LEAN_AND_MEAN #include #include #include #include #include // Using wmain and /MD prevents VC6 from using wsetargv; fortunately, it // doesn't take much to fix. int _dowildcard = 1; #define VIEWER_ENV L"VIEW-ER" // "VIEWER" might be too common #define VIEWER L"less" // If we're going to use CMD.EXE's list, may as well use its name, too. static const WCHAR szSpecialFileCharsToQuote[] = L" &()[]{}^=;!%'+,`~"; #if defined(__GNUC__) #define NORETURN __attribute__((noreturn)) #elif defined(_MSC_VER) #define NORETURN __declspec(noreturn) #else #define NORETURN #endif NORETURN void help( void ); NORETURN void viewer( int, LPWSTR [] ); NORETURN void ErrorExit( int, LPCWSTR, const char* ); NORETURN void viewpipe( char*, int ); void help( void ) { puts( "\ View by Jason Hood .\n\ Version " PVERS " (" PDATE "). Public Domain.\n\ http://misc.adoxa.vze.com/\n\ \n\ Display a file (or standard input if none) to the screen if it fits,\n\ otherwise use a viewer. Having more than one file or redirecting output will\n\ also use the viewer. The default viewer is \"less\"; the environment variable\n\ \"VIEW-ER\" can be set to use something else (eg: \"set VIEW-ER=more.com\").\ " ); exit( 0 ); } void ErrorExit( int rc, LPCWSTR msg1, const char* msg2 ) { char buf[MAX_PATH * 2]; DWORD len; // Writing wide strings with stdio is basically useless. Since it's only a // file name, keep it simple. if (_isatty( 2 )) { fprintf( stderr, "view: " ); WriteConsoleW( GetStdHandle( STD_ERROR_HANDLE ), msg1, (DWORD)wcslen( msg1 ), &len, NULL ); fprintf( stderr, "%s.\n", msg2 ); } else { WideCharToMultiByte( GetConsoleOutputCP(), 0, msg1, -1, buf, sizeof(buf), NULL, NULL ); fprintf( stderr, "view: %s%s.\n", buf, msg2 ); } exit( rc ); } // Copy SRC to DST, returning a pointer to the NUL in dst. LPWSTR wcpcpy( LPWSTR dst, LPCWSTR src ) { while ((*dst = *src++) != '\0') ++dst; return dst; } void viewer( int argc, wchar_t* argv[] ) { LPWSTR vwr, cmdline; LPWSTR arg; size_t len; int i; PROCESS_INFORMATION pi; STARTUPINFO si; DWORD rc; vwr = _wgetenv( VIEWER_ENV ); if (vwr == NULL) vwr = VIEWER; len = wcslen( vwr ); for (i = 1; i < argc; ++i) { len += 1 + wcslen( argv[i] ); if (wcspbrk( argv[i], szSpecialFileCharsToQuote ) != NULL) len += 2; } cmdline = malloc( (len + 1) * sizeof(WCHAR) ); arg = wcpcpy( cmdline, vwr ); for (i = 1; i < argc; ++i) { *arg++ = ' '; if (wcspbrk( argv[i], szSpecialFileCharsToQuote ) == NULL) { arg = wcpcpy( arg, argv[i] ); } else { *arg++ = '"'; arg = wcpcpy( arg, argv[i] ); *arg++ = '"'; } } *arg = '\0'; ZeroMemory( &si, sizeof(STARTUPINFO) ); si.cb = sizeof(STARTUPINFO); if (!CreateProcess( NULL, cmdline, NULL,NULL, FALSE, 0, NULL,NULL, &si, &pi )) ErrorExit( 3, vwr, ": unable to execute" ); WaitForSingleObject( pi.hProcess, INFINITE ); GetExitCodeProcess( pi.hProcess, &rc ); exit( rc ); } void viewpipe( char* buf, int size ) { LPWSTR vwr; int ch, len; DWORD rc, wrote; BOOL wait; // Taken directly from the Win32 Programmer's Reference ("Creating a Child // Process with Redirected Input and Output"). HANDLE hChildStdinRd, hChildStdinWr, hChildStdinWrDup, hSaveStdin; SECURITY_ATTRIBUTES saAttr; PROCESS_INFORMATION piProcInfo; STARTUPINFO siStartInfo; saAttr.nLength = sizeof(SECURITY_ATTRIBUTES); saAttr.bInheritHandle = TRUE; saAttr.lpSecurityDescriptor = NULL; hSaveStdin = GetStdHandle( STD_INPUT_HANDLE ); if (!CreatePipe( &hChildStdinRd, &hChildStdinWr, &saAttr, 0 )) ErrorExit( 4, L"", "Stdin pipe creation failed" ); if (!SetStdHandle( STD_INPUT_HANDLE, hChildStdinRd )) ErrorExit( 4, L"", "Redirecting Stdin failed" ); if (!DuplicateHandle( GetCurrentProcess(), hChildStdinWr, GetCurrentProcess(), &hChildStdinWrDup, 0, FALSE, DUPLICATE_SAME_ACCESS )) ErrorExit( 4, L"", "DuplicateHandle failed" ); CloseHandle( hChildStdinWr ); vwr = _wgetenv( VIEWER_ENV ); if (vwr == NULL) vwr = VIEWER; ZeroMemory( &siStartInfo, sizeof(STARTUPINFO) ); siStartInfo.cb = sizeof(STARTUPINFO); if (!CreateProcess( NULL, vwr, NULL, NULL, TRUE, 0, NULL, NULL, &siStartInfo, &piProcInfo )) ErrorExit( 3, vwr, ": unable to execute" ); if (!SetStdHandle( STD_INPUT_HANDLE, hSaveStdin )) ErrorExit( 4, L"", "Re-redirecting Stdin failed" ); rc = PIPE_NOWAIT; SetNamedPipeHandleState( hChildStdinWrDup, &rc, NULL, NULL ); rc = STILL_ACTIVE; len = size; do { wait = FALSE; while (len > 0) { if (!WriteFile( hChildStdinWrDup, buf, len, &wrote, NULL )) goto done; if (wrote == 0) { if (wait) { GetExitCodeProcess( piProcInfo.hProcess, &rc ); if (rc != STILL_ACTIVE) goto done; Sleep( 10 ); } else { wait = TRUE; } } else { len -= wrote; } } do { ch = getchar(); if (ch == EOF) break; buf[len++] = ch; } while (ch != '\n' && len < size); } while (len > 0); done: if (!CloseHandle( hChildStdinWrDup )) ErrorExit( 4, L"", "Close pipe failed" ); if (rc == STILL_ACTIVE) { WaitForSingleObject( piProcInfo.hProcess, INFINITE ); GetExitCodeProcess( piProcInfo.hProcess, &rc ); } exit( rc ); } #if defined(__MINGW32__) && !defined(__MINGW64_VERSION_MAJOR) // Thanks to Coder for Life. // http://www.coderforlife.com/projects/utilities/ int wmain(); void __wgetmainargs( int*, wchar_t***, wchar_t***, int, int* ); int main() { wchar_t **argv, **envp; int argc, si = 0; __wgetmainargs( &argc, &argv, &envp, _dowildcard, &si ); return wmain( argc, argv ); } #endif int wmain( int argc, wchar_t* argv[] ) { int lines, cols; FILE* file; char* buf; int max, out; int ch, len; BOOL bs; // Use two buffers: one for writing to screen (where newlines are removed // for lines of screen width) and one for the pipe (keeping the newlines). BOOL pipe; char* pbuf; int pout; CONSOLE_SCREEN_BUFFER_INFO csbi; HANDLE scrn; DWORD wr; char* line; SHORT y; COORD s, c; SMALL_RECT r; CHAR_INFO blank; PCHAR_INFO row; if ((argc == 1 && _isatty( 0 )) || (argc == 2 && (wcscmp( argv[1], L"/?" ) == 0 || wcscmp( argv[1], L"-?" ) == 0 || wcscmp( argv[1], L"--help" ) == 0))) help(); if (argc == 2 && wcscmp( argv[1], L"--version" ) == 0) { puts( "View version " PVERS " (" PDATE ")." ); return 0; } if (argc > 2 || !_isatty( 1 ) || (argc == 2 && !_isatty( 0 ))) viewer( argc, argv ); if (argc == 1) file = stdin; else if ((file = _wfopen( argv[1], L"r" )) == NULL) ErrorExit( 1, argv[1], ": unable to open" ); GetConsoleScreenBufferInfo( GetStdHandle( STD_OUTPUT_HANDLE ), &csbi ); lines = csbi.srWindow.Bottom - csbi.srWindow.Top + 1 - 2; // blank & prompt cols = csbi.dwSize.X; scrn = CreateConsoleScreenBuffer( GENERIC_READ | GENERIC_WRITE, 0, NULL, CONSOLE_TEXTMODE_BUFFER, NULL ); // Buffers are created the size of the current window, so extend the columns // to the current buffer. if (csbi.srWindow.Right - csbi.srWindow.Left + 1 != cols) { csbi.dwSize.Y = lines + 2; SetConsoleScreenBufferSize( scrn, csbi.dwSize ); } // Allow four bytes per character, plus an LF each line. max = (lines + 1) * (cols + 1) * 4; pipe = (GetFileType( GetStdHandle( STD_INPUT_HANDLE ) ) == FILE_TYPE_PIPE); if (pipe) max *= 2; buf = malloc( max + cols * sizeof(CHAR_INFO) ); if (buf == NULL) ErrorExit( 2, L"", "not enough memory" ); row = (PCHAR_INFO)(buf + max); pbuf = buf; pout = 0; if (pipe) { max /= 2; pbuf += max; } // Variables for detecting wrap. s.X = cols; s.Y = 1; c.Y = 0; r.Left = 0; r.Right = cols - 1; blank.Char.UnicodeChar = ' '; blank.Attributes = csbi.wAttributes; y = 0; bs = FALSE; len = out = 0; line = buf; while ((ch = fgetc( file )) != EOF && out < max) { buf[out++] = ch; pbuf[pout++] = ch; if (ch == '\n') { LPCWSTR nl = L"\n"; WriteConsoleA( scrn, line, len, &wr, NULL ); len = 0; GetConsoleScreenBufferInfo( scrn, &csbi ); if (csbi.dwCursorPosition.Y > y) { // It's wrapped, but was anything more written? Look at the current // row, checking that each character is space in current attributes. // If it's all blank we can drop the newline. If the cursor isn't // already at the margin, then it was spaces or tabs that caused the // wrap, which can be ignored and overwritten. c.X = 0; r.Top = r.Bottom = csbi.dwCursorPosition.Y; ReadConsoleOutput( scrn, row, s, c, &r ); while (*(PDWORD)&row[c.X] == *(PDWORD)&blank) if (++c.X == s.X) { --out; nl = (csbi.dwCursorPosition.X == 0) ? NULL : L"\r"; break; } } if (nl) { WriteConsole( scrn, nl, 1, &wr, NULL ); GetConsoleScreenBufferInfo( scrn, &csbi ); } if (csbi.dwCursorPosition.Y > lines) { out = max; break; } y = csbi.dwCursorPosition.Y; line = buf + out; } else if (ch == '\b') { // The console has some weird issue with backspace, so if we see any // we'll flush. For example: "a.\b1\nb.\b2\nc.\b3\n" correctly // displays "a1\nb2\n", but then it comes up with "3.\n". bs = TRUE; WriteConsoleA( scrn, line, len, &wr, NULL ); line += len; len = 1; } else if (ch == '\a') { // Don't want to hear the bell twice. WriteConsoleA( scrn, line, len, &wr, NULL ); line = buf + out; len = 0; } else { ++len; } } if (len != 0) { WriteConsoleA( scrn, line, len, &wr, NULL ); GetConsoleScreenBufferInfo( scrn, &csbi ); if (csbi.dwCursorPosition.Y > lines) out = max; } CloseHandle( scrn ); if (file != stdin) fclose( file ); if (out >= max) { if (pipe) viewpipe( pbuf, pout ); if (file == stdin) rewind( stdin ); free( buf ); viewer( argc, argv ); } scrn = GetStdHandle( STD_OUTPUT_HANDLE ); if (bs) { line = buf; len = 0; while (len < out) { if (line[len] == '\b') { WriteConsoleA( scrn, line, len, &wr, NULL ); out -= len; line += len; len = 0; } ++len; } if (len != 0) WriteConsoleA( scrn, line, len, &wr, NULL ); } else { WriteConsoleA( scrn, buf, out, &wr, NULL ); } return 0; }